mongooseの埋め込みオブジェクトの変更に伴う保存について

node.js + mongo + express で遊んでいます。
@snamuraさんも言っていたとおり、鼻血が出るほどnode.js + mongodbは相性がよく、また、mongooseがよく出来ています。

以下mongoose 1.0.13の利用例

var mongoose = require('mongoose'),
      Schema = mongoose.Schema,
      User, Entry,
      crypto = require('crypot'),
      md5 = function(data){
          var hash = crypto.createHash('md5');
          hash.update(data);
          return hash.digest('hex');
      };

mongoose.model('User', User); User = mongoose.model('User');

User = new Schema({
  name: {type: String, index: {unique: true}},
  pass: String
});

mongoose.connect('mongodb://localhost/blogdb');

var users = 'muddydixon:muddy hogehoge:hoge fugafuga:fuga'. // username:password
  split(/\s/).
  map(function(user){
     user = user.split(/:/);
     var _user = new User({name: user[0], pass: md5(user[1])});
     _user.save();
  });

とここまでがUserの登録なんですが、

10gen - Upcoming MongoDB and NoSQL conferences, webinars, and meetups
でも@rogerbさんが例を上げていたように、この後、Entryの組み立て方にいくつか選択肢があります。

  • 以下のやり方だと、User書き込みの完了を待たずにBlogを登録しだすので、2回(Userを作るのだけ成功、Blogをつくるのに成功)実行する必要があります

mongooseのembeddedどおりのやり方

Entry = new Schema({
  author: [User],
  title: {type: String, index: {unique: true}},
  body: String,
  created: {type: Date, default: Date.now}
});
mongoose.model('Entry', Entry); Entry = mongoose.model('Entry');

var entries = 'muddydixon:Spring:hasCome muddydixon:Summer:hasCome! hogehoge:Autum:hasComeAndGone'. // authorname:title:body
  split(/\s/).
  map(function(entry){
    entry = entry.split(/:/);
    User.findOne({name: entry[0]}, function(err, user){
      if(!err && user){
        var _entry = new Entry({author: [{name: user.name, pass: user.pass}], title: entry[1], body: entry[2]});
        entry.save();
      }
    })
  });
  • このやり方だと下のような方法で検索できることがメリットです。が、UserはUserでつくって、埋込みでさらに作る(db.usersとdb.blogsで同じ名前で同じパスだけど、別のObjectIdのレコードができてしまいます)ので、なんか冗長だし、ダサいのでなんかやです
mongo> db.blogs.find({"author.name": "muddydixon"})

ObjectIdでひもづける

Entry = new Schema({
  author: [mongoose.ObjectId],
  title: {type: String, index: {unique: true}},
  body: String,
  created: {type: Date, default: Date.now}
});
mongoose.model('Entry', Entry); Entry = mongoose.model('Entry');

var entries = 'muddydixon:Spring:hasCome muddydixon:Summer:hasCome! hogehoge:Autum:hasComeAndGone'. // authorname:title:body
  split(/\s/).
  map(function(entry){
    entry = entry.split(/:/);
    User.findOne({name: entry[0]}, function(err, user){
      if(!err && user){
        var _entry = new Entry({author: [user._id], title: entry[1], body: entry[2]});
        entry.save();
      }
    })
  });
  • このやり方だと、当然、user.nameをキーにdb.blogs.findすることは出来ません。が、usernameの変更に対しても対応が可能です。
  • あと、正規化を考えるのであれば、非常に気持が良いです。

問題!

  • 最初のケースでは、「authorを付け替える」=「author[0].remove()」+「author.push({name: name, pass:pass})」をしてsaveするとmongoの中身は反映されません。
    • remove→saveのfunction内で追記してsaveすれば正常になります。けどだりー!!
  • 削除のみ/追加のみ、だと正常に反映されるのですが両者を行うと反映されないようです。
  • あとこういうのもあります
  • 2番目のケースでも、embeddedをいじって、saveしても変化が反映されません。
  • これは、__setter__のレベルで検知できおらず、embeddedの方で反映があったことを検知して埋め込んでいるオブジェクトに伝達する必要があります
  • − とりあえずの回避策としては下記のようにすることができます
blog.activePaths.states.modify.author = true;
    • activePaths.states.modify objectには、フィールドの監視をして変更があったものを追記していきます。
    • ですが、embeddedなオブジェクトの変化はそこまでエスカレーションしていかないようです。
    • なのでじょうきのやりかたで強制的に「変化あったよ!」と伝えるのです。
  • 取り急ぎそんな感じです。