node.js から MongoDB にアクセス (Mongoose の紹介)

node.js から MongoDB にアクセススためのライブラリに Mongoose があります.今回はこれを紹介しようと思います.O/R Mapper っぽく使えるように設計されており,既存の O/R Mapper を使ったことがある人にとっては,比較的わかりやすい仕様です.

見出し

  • Mongoose とは?
  • インストール
  • 何はともあれ使い方を
  • Schema 定義について
  • ドキュメント生成 (保存)
  • ドキュメント読み取り
  • ドキュメント更新
  • ドキュメント削除
  • Embedded Document
  • 終わりに

Mongoose とは?

node.js 向けに開発された MongoDB アクセスライブラリです.

Mongoose is a MongoDB object modeling tool designed to work in an asychronous environment.
Mongoose とは,非同期環境において動作するよう設計された,MongoDB オブジェクト用モデリングツールである.[引用]

MongoDB に格納するモデルを定義し,O/R Mapper の様なノリで操作できるようになります.なお,あくまで "ノリ" であって,node.js で動く以上はイベント駆動なんだということは留意しておくべきです.

インストール

$ npm install mongoose

パッケージ管理があると楽だ.

何はともあれ使い方を

MongoDB のホストが localhost,データベースが sample_db であったとします.

var mongoose = require('mongoose');

// 定義フェーズ
var Schema   = mongoose.Schema;

var UserSchema = new Schema({
  name:  String,
  point: Number
});
mongoose.model('User', UserSchema);

// 使用フェーズ
mongoose.connect('mongodb://localhost/sample_db');

var User = mongoose.model('User');
var user = new User();
user.name  = 'KrdLab';
user.point = 777;
user.save(function(err) {
  if (err) { console.log(err); }
});

// ※注意:イベント駆動

User.find({}, function(err, docs) {
  for (var i=0, size=docs.length; i<size; ++i) {
    console.log(docs[i].doc.name);
  }
});

(個人的に,JavaScript において 'new' を使うオブジェクト生成は嫌いなのですが...今回はこらえて流儀に従います.)

上記のように User スキーマを定義 → save の流れでドキュメントが格納されます.なお,MongoDB の各ドキュメントに割り振られる '_id' は,そのまま 'user._id' で取得可能です (Schema.ObjectId オブジェクトとして扱われる).

find には (query, fields, options, callback) の 4 引数があります.query は必須,残りは省略可能です.ただし,callback を省略すると FindQuery オブジェクトが返ってきます.このオブジェクトはクエリのコンテキストであり,DataMapper (PofEAA のパターン名ではなく,ruby 向けの DataMapper) でいうところの Scopes and Chaining に相当する操作を可能とします (where や sort,skip,limit 等を積み重ねて,exec すると実際にクエリが発行される).
他にも,findOne,findById 等ありますが,詳しくは API の Model を参照してください.

上記サンプルでは save のあとすぐに find していますが,これは必ずしもうまくいきません (クエリが submit されても,どちらが先に終わるかわからないため).実際には save のハンドラ内で登録されるクロージャの中で find をすることになります.この辺は Reactor Pattern やらイベント駆動やらではおなじみです.

補足ですが,connect の呼び出しは使用直前まで遅らせることができます.

Mongoose buffers all the commands until it's connected to the database.
Mongoose は,データベースへの接続が確立されるまで,全てのコマンドをバッファする.[引用]

Schema 定義について

Validation をかけたり,default 値を設定できます (Model Definition).ここら辺は DataMapper の "property :hoge ..." の感じと同じです.

var User = new Schema({
  first_name: { type: String, required: true },
  last_name:  { type: String, required: true },
  point:      { type: Number, min: 0, default: 0 },
  created:    { type: Date, default: Date.now },
  updated:    { type: Date, default: Date.now }
});

Validation は save のタイミングでかかります.上記の例では,'first_name','last_name' は値を指定しないとエラー,'point' は 0 未満を指定するとエラー,といった感じです.
default 値は new のタイミングでオブジェクトに積まれます.上記の例では,'point' は 0,'created','updated' は現在の日時が格納されます.

MongoDB はスキーマレスが特徴の一つですが,要件として Validation をかけたい場合もあるでしょう.また,default 値指定は便利です.

ドキュメント生成 (保存)

先ほど出てきた通り.

User = mongoose.model('User');
var user = new User();
user.name  = 'KrdLab';
user.point = 777;

// user = new User({ name: 'KrdLab', point: 777 }); も可能

user.save(function(err) {
  if (err) { console.log(err); }
});

ドキュメント読み取り

まずは all 指定.

User.find({}, function(err, docs) {
  for (var i=0, size=docs.length; i<size; ++i) {
    // docs[i].doc
  }
});

次に query 指定.
指定形式は Querying とほぼ同じようです.

// SELECT * FROM users WHERE name='KrdLab' に相当
User.find({ name: 'KrdLab' }, function(err, docs) {
  // マッチしたドキュメントが docs[i].doc で取れる
});

そして,query + フィールド指定.

// SELECT point FROM users WHERE name='KrdLab' に相当
User.find({ name: 'KrdLab' }, ['point'], function(err, docs) {
  // マッチしたドキュメントが docs[i].doc で取れ,フィールドは point のみ
  // (default 値を指定したフィールドは,fields 引数に指定していなくても「default 値」が取れることに注意)
});

最後に,query + フィールド + オプション指定.
オプションには,limit,skip,sort 等が指定できます.指定形式は Advanced Queries とほぼ同じようです.

// SELECT point FROM users WHERE name='KrdLab' LIMIT 2 に相当
User.find({ name: 'KrdLab' }, ['point'], { limit: 2 }, function(err, docs) {
  // 略 (とにかく docs[i].doc で取れる)
});

ドキュメント更新

第 2 引数には Modifier Operations を指定します.

// UPDATE users SET point=999 WHERE name='KrdLab' に相当
User.update({ name: 'KrdLab' }, { $set: {point: 999} },
            { upsert: false, multi: true }, function(err) {
  // ...
});

補足:MongoDB は Atomic Operations にある通り,単一ドキュメントに対しのみ Atomic な操作を保証しています.lock や transaction はサポートされていません (v1.2.2).理由,並びに代替方法については前述のリンク先に記載されています.

ドキュメント削除

第 1 引数にクエリを指定します.'{}' 指定だと...全部消えます.

// DELETE FROM users WHERE name='KrdLab' に相当
User.remove({ name: 'KrdLab' }, function(err) {
  // ...
});

Embedded Document

MongoDB にはドキュメントの中にドキュメントを埋め込む embedded document という概念があります (Schema Design).Mongoose でも当然サポートされており,Schema 定義は以下のようになります.

// 公式のサンプルを若干修正
var CommentSchema = new Schema({
  title:  String,
  body:   String,
  date:   Date
});

var BlogPostSchema = new Schema({
  author: ObjectId,
  title:  String,
  body:   String,
  date:   Date,
  comments: [CommentSchema],
  metadata: {
    like: Number,
  }
});
mongoose.model('BlogPost', BlogPostSchema);

'comments' のように外部に Schema を定義して埋め込む方法と,'metadata' のようにスキーマ定義ごと埋め込む方法の 2 つがあります.なお,'metadata' のような埋め込みを行った場合は,BlogPost のコンストラクタで値を設定する必要があり,その後は get しかできないようです (要確認).
既存の BlogPost に対して comments を更新する場合は以下のように行います.

// 投稿済み BlogPost の _id を 'post_id' とする
BlogPost.findOne({ _id: post_id }, function(err, post) {
  if (!err) {
    var comment = {};
    comment.title = 'タイトル';
    comment.body  = '内容';
    comment.date  = Date.now();
    post.comments.push(comment);  // 追加
    post.save(function(err) {
      // ...
    });
  } else {
    console.log('error findOne: ' + err);
  }
});

新しいコメントを追加する場合,Comment を使わずに Object を生成し,push する必要があります (Comment を使って push すると RangeError が発生する).
save を使った場合は,追加した埋め込みドキュメント (comment) に '_id' が割り当てられます.しかし,以下のように update を使った場合は,単純に Object がそのまま追加されるのみであり,'_id' は割り当てられないようです.

var comment = {};
// ... set fields ...
BlogPost.update({ _id: post_id }, { $push: { comments: comment } },
                { upsert: false, multi: false }, function(err) {
  // ...
});

save 以外使うなってことか...

また,公式にて embedded document の削除は post.comments[0].remove() のように 'remove' 呼べよと紹介されていますが,呼び出しても消えません (v1.1.2).なぜ?

終わりに

Mongoose の I/F は,既存の O/R Mapper を使ったことのある人,または MongoDB のシェルをさわったことのある人が,比較的容易に使えるような感じで設計されています.
そしてやはり node.js と MongoDB は相性が良いですね.データの受け渡しが全て JSON で済んでしまうというのは楽です.
しばらくあれこれ使ってみようと思います.