ラベル MongoDB の投稿を表示しています。 すべての投稿を表示
ラベル MongoDB の投稿を表示しています。 すべての投稿を表示

2012年12月18日火曜日

Node.jsが面白い件⑦ MongoDBで遊ぶ 〜Mongoose編その④:Middleware〜

今回はMongooseの便利な機能の1つ、middlewareを使ってみます。middlewareにはpreとpostがあり、initやsave、validate、removeの前後の処理をSchemaレベルで定義することができます。

4. pre

まずは前処理のpreから記述してみます。今まで使ってきたuserSchemaを使いますが、わかりやすくするためにcreatedキーを付けます。

    // サンプルスキーマ
    var userSchema  = mongoose.Schema({
        uid : { type: String, index: {unique:true} },
        name: { first: String, last: String },
        age : { type: Number, min: 1 },
        gender : { type: String, enum: genders },
        created : Date
    });

    // preの設定
    userSchema.pre('save', function(next) {
      this.created = new Date;
      next();
    });

この例では、preを使ってsaveされる直前に現在時刻をcreatedに設定する処理を記述しています。それではsaveしてみます。

    // インスタンス(document)作成。
    var user = new User();
    user.uid = 'kat';
    user.name.first = 'KAT';
    user.name.last = 'INTINK';
    user.age = 100;
    user.gender = 'male';

    user.save(function(err) {(以下、省略)

以下、結果です。

[ { created: Tue Dec 18 2012 22:45:22 GMT+0900 (JST),
    gender: 'male',
    age: 100,
    uid: 'kat',
    _id: 50d07372ff75973d29000001,
    __v: 0,
    name: { last: 'INTINK', first: 'KAT' } } ]

createdは事前に設定していませんが、きちんと入っていることがわかります。preで定義した関数の中でnext()が呼ばれていますが、next()が呼ばれることで次の処理が実行されます。複数のpre定義があった場合、next()が呼ばれるたびに次の処理が実行されることになります。

上の例では順次実行でしたが、処理によっては複数の処理を並列で処理したい場合もあります。この場合は以下のようにnext()/done()を使います。

    var checkStr = "a";

    // preの設定
    // 処理①
    userSchema.pre('save', true, function(next, done) {
      console.log(new Date);
      next();    // ->処理②の開始。
      setTimeout(function() {
        checkStr += 'b';
        done();  // ->処理①の終了。
      }, 1000);
    });

    // 処理②
    userSchema.pre('save', true, function(next, done) {
      console.log(new Date);
      next();    // ->処理③の開始。
      setTimeout(function() {
        checkStr += 'c';
        done();  // ->処理②の終了。
      }, 2000);
    });

    // 処理③
    userSchema.pre('save', true, function(next, done) {
      console.log(new Date);
      next();    // ->終了次第、実際の処理(save)
      setTimeout(function() {
        checkStr += 'd';
        done();  // ->処理③の終了。
      }, 3000);
    });

実際の処理ではこのような書き方をしないと思いますが、動作がわかりやすいように各preの定義でsetTimeoutによって処理を実行しています。各関数の中にはnext()とdone()がそれぞれ記述されているのがわかります。next()が呼ばれると前述のように次の処理が実行されますが、それぞれの処理は終了時にdone()を実行しています。この場合、全てのdone()が実行されないとsave処理を実施しませんが、それぞれは並列で実行されるため効率よく処理が終了できます。また、並列で実行する際にはpreの第二引数にtrueの指定を忘れないようにしましょう。念のため、結果は以下のようになります。

Tue Dec 18 2012 22:58:15 GMT+0900 (JST) // -> 処理①開始
Tue Dec 18 2012 22:58:15 GMT+0900 (JST) // -> 処理②開始
Tue Dec 18 2012 22:58:15 GMT+0900 (JST) // -> 処理③開始
Tue Dec 18 2012 22:58:18 GMT+0900 (JST) // -> save開始
checkStr : abcd

上手く使えばいろいろと効率よく処理ができそうですね。ドキュメントには複雑なバリデーションや依存関係のあるドキュメントの削除などが例として記載されています。

4. post

事前処理があれば事後処理も、ということでpostが用意されています。postはよりシンプルで、以下のように定義できます。

    // postの設定
    userSchema.post('save', function(doc) {
      console.log('User: %s was created.', doc.uid);
    });

実行結果は以下のようになります。

User: kat was created.

pre/postを使えばRDBMSで言うトリガーのような処理が実現できます。あまり複雑な処理は重くなるため好ましくないですが、ODMの機能を効果的に使うことでそこそこの実装コスト削減が期待できそうですね。

2012年12月12日水曜日

Node.jsが面白い件⑥ MongoDBで遊ぶ 〜Mongoose編その③:サブドキュメントとリファレンス〜

前回に引き続き、今回はサブドキュメントとリファレンスについてです。MongoDBではEmbedding and Linkingとして解説されており、Joinを持たないMongoDBの設計手法の1つとしてよく使われます。

2. sub-docs

MongooseではSchemaを定義してドキュメントを操作しますが、別のドキュメント配列をSchemaの一部として保持することができます。以下、これまで使用していたuserSchemaにhistSchemaというドキュメントを持たせました。

    // SubDocument Schema
    var operations = 'create,submit,cancel'.split(',');
    var histSchema = mongoose.Schema({
        operation : { type: String, enum: operations },
        date: Date
    });

    // サンプルスキーマ
    var userSchema  = mongoose.Schema({
        uid : { type: String, index: {unique:true} },
        name: { first: String, last: String },
        age : { type: Number, min: 1 },
        gender : { type: String, enum: genders },
        histories : [ histSchema ]
    });

    var User = db.model('User', userSchema);

userドキュメント作成の際に、historiesの値を入れます。

    User.create(
      { uid: 'kat',
        name: {first: 'KAT', last:'INTINK'},
        age: 100,
        gender: 'male',
        histories: [{ operation : 'create', date: new Date }]
      }, (以下省略)

値を表示すると以下のように_idが自動生成され、ドキュメントとして保持されていることがわかります。
{ uid: 'kat',
  age: 100,
  gender: 'male',
  _id: 50c5fa1329c6e5d70c000002,
  __v: 0,
  histories: 
   [ { operation: 'create',
       date: Tue Dec 11 2012 00:04:51 GMT+0900 (JST),
       _id: 50c5fa1329c6e5d70c000003 } ],
  name: { first: 'KAT', last: 'INTINK' } }

さらにドキュメントを追加します。

user.histories.push({ operation: 'submit', date: new Date });
user.save (function (err) { (以下省略)

{ uid: 'kat',
  age: 100,
  gender: 'male',
  _id: 50c5fae2afa091e00c000002,
  __v: 1,
  histories: 
   [ { operation: 'create',
       date: Tue Dec 11 2012 00:04:51 GMT+0900 (JST),
       _id: 50c5fa1329c6e5d70c000003 },
     { operation: 'submit',
       date: Tue Dec 11 2012 00:08:18 GMT+0900 (JST),
       _id: 50c5fae2afa091e00c000004 } ],
  name: { first: 'KAT', last: 'INTINK' } }

内部に保持されるドキュメントなので、あるオブジェクトが複数の固有のドキュメントを持つような場合に使えます(例だとユーザの操作ログを保持しています)。外部のドキュメントを参照したい場合は、次に説明するリファレンスを使います。

3. population

以下のようなSchemaを考えます。

    // itemSchema
    var itemSchema = new mongoose.Schema({
      name : String,
      color : String
    });

    // サンプルスキーマ
    var userSchema  = mongoose.Schema({
      uid : { type: String, index: {unique:true} },
      name: { first: String, last: String },
      age : { type: Number, min: 1 },
      gender : { type: String, enum: genders },
      histories : [ histSchema ],
      items : [ { type: mongoose.Schema.Types.ObjectId, ref: 'Item' } ]
    });

    var Item = db.model('Item', itemSchema);
    var User = db.model('User', userSchema);

sub-docsで使用したuserSchemaに、itemsを持たせました。ただし、itemsはitemSchemaとして定義されているモデルItemを参照しています。それではドキュメントを作ってみましょう。まずはitemから。

    Item.create(
      { name : 'table', color : 'black' },
      function(err, item1) {
        if (err) {
          console.log('[error] create item');
          console.log(err);
        } else {
          Item.create(
            { name: 'chair', color: 'white' },
            function(err, item2) { (以下省略)

上記のようにitemを2つ作った後、Userにこれら2つのドキュメントをセットします。Userを作ったあとsub-docsと同様にpushします。

    User.create(
      { uid: 'kat',
        name: {first: 'KAT', last:'INTINK'},
        age: 100,
        gender: 'male',
        histories: [{ operation : 'create', date: new Date }],
      }, function(err, user) { 

(省略)

                  user.items.push(item1);
                  user.items.push(item2);

                  user.save (function(err) {


(以下省略)

結果を見てみます。まずはUserを見ると、以下のように格納されています。

User.findOne().exec(

{ uid: 'kat',
  age: 100,
  gender: 'male',
  _id: 50c886e21f32c23d18000004,
  __v: 2,
  items: [ 50c886e21f32c23d18000002, 50c886e21f32c23d18000003 ],
  histories: 
   [ { operation: 'create',
       date: Wed Dec 12 2012 22:30:10 GMT+0900 (JST),
       _id: 50c886e21f32c23d18000005 },
     { operation: 'submit',
       date: Wed Dec 12 2012 22:30:10 GMT+0900 (JST),
       _id: 50c886e21f32c23d18000006 } ],
  name: { first: 'KAT', last: 'INTINK' } }

itemsに表示されている結果は、pushにより格納されたitem1、item2の_idです。これだけだと単にitemの_idを保持しているだけですが、mongooseではpopulateを使ってJoinのように各_idに相当する値を一緒に取得することができます。

User.findOne().populate('items').exec(function(err, items) {

この結果は以下のようになります。

{ __v: 2,
  _id: 50c886e21f32c23d18000004,
  age: 100,
  gender: 'male',
  uid: 'kat',
  items: 
   [ { name: 'table',
       color: 'black',
       _id: 50c886e21f32c23d18000002,
       __v: 0 },
     { name: 'chair',
       color: 'white',
       _id: 50c886e21f32c23d18000003,
       __v: 0 } ],
  histories: 
   [ { operation: 'create',
       date: Wed Dec 12 2012 22:30:10 GMT+0900 (JST),
       _id: 50c886e21f32c23d18000005 },
     { operation: 'submit',
       date: Wed Dec 12 2012 22:30:10 GMT+0900 (JST),
       _id: 50c886e21f32c23d18000006 } ],
  name: { first: 'KAT', last: 'INTINK' } }

itemsの中身が、_idだけではなくname, colorの値も表示されました。このように、Joinに似た操作を使ってより自然なドキュメントの結果を扱うことができます。

以上、ドキュメント内で他のドキュメントを扱うことでBlogとコメントなど自然な形のオブジェクトを表現することが可能になります。

2012年12月9日日曜日

Node.jsが面白い件⑤ MongoDBで遊ぶ 〜Mongoose編その②:バリデーション〜

Node.jsが面白い件④ MongoDBで遊ぶ 〜Mongoose編その①〜ではMongooseの基本的な使い方を説明しましたが、今回から数回に分けてMongooseの便利な機能について紹介します。

1. Validator

Mongooseではバリデーション機能として次のようなビルトインバリデータを提供します。
  1. required
  2. 最大/最小(数値)
  3. enum/マッチング(文字列)
前回のスキーマに対して上記のビルトインバリデータを試してみます。まずは前回のスキーマの復習ですが、

   var userSchema  = mongoose.Schema({
        uid : { type: String, required:true, index: {unique:true} },
        name: { first: String, last: String },
        age : { type: Number, min: 1 },
        gender : { type: String, enum: genders }
   });

です。これに、以下のドキュメントを作成します。

   User.create(
      { name: {first: 'KAT', last: 'INTINK'},
        age: 100,
        gender: 'male'
      },(以下省略)

uidを指定していないため、requiredにひっかかり以下のようなエラーとなります。
{ message: 'Validation failed',
  name: 'ValidationError',
  errors: 
   { uid: 
      { message: 'Validator "required" failed for path uid',
        name: 'ValidatorError',
        path: 'uid',
        type: 'required' } } }

次に、ageを-1にしてみます。

    User.create(
      { uid: 'kat',
        name: {first: 'KAT', last: 'INTINK'},
        age: -1,
        gender: 'male'
      }, (以下省略)

minにひっかかり以下のようなエラーとなります。
{ message: 'Validation failed',
  name: 'ValidationError',
  errors: 
   { age: 
      { message: 'Validator "min" failed for path age',
        name: 'ValidatorError',
        path: 'age',
        type: 'min' } } }

genderにenum以外の値、manを指定してみます。

    User.create(
      { uid: 'kat',
        name: {first: 'KAT', last: 'INTINK'},
        age: 100,
        gender: 'man'
      }, (以下省略)

同様に以下のエラーとなります。
{ message: 'Validation failed',
  name: 'ValidationError',
  errors: 
   { gender: 
      { message: 'Validator "enum" failed for path gender',
        name: 'ValidatorError',
        path: 'gender',
        type: 'enum' } } }

また、カスタムバリデータとして以下のように定義することも可能です。

    var userSchema  = mongoose.Schema({
        uid : { type: String, required:true, index: {unique:true} },
        name: { first:
                    {type: String,
                     validate: [function(fname) {
                                   return fname.length < 5;
                               }, 'MaxNameLength']
                    },
                last: String },
        age : { type: Number, min: 1 },
        gender : { type: String, enum: genders }
    });

firstnameが5以上だとMaxNameLengthにひっかかります。試しに以下のドキュメントを作成します。

    User.create(
      { uid: 'kat',
        name: {first: 'KATXX', last: 'INTINK'},
        age: 100,
        gender: 'male'
      }, (以下省略)


以下のようにエラーが発生しました。
{ message: 'Validation failed',
  name: 'ValidationError',
  errors: 
   { 'name.first': 
      { message: 'Validator "MaxNameLength" failed for path name.first',
        name: 'ValidatorError',
        path: 'name.first',
        type: 'MaxNameLength' } } }

バリデーション機能は便利というよりほぼ必須機能になってますが、上記のように簡単に提供された機能を使って実現することができます。

2012年12月8日土曜日

Node.jsが面白い件④ MongoDBで遊ぶ 〜Mongoose編その①〜

今回はNode.js+MongoDB環境でよく使われるMongooseを使ってみたいと思います。MongooseはODM(Object document mapper)の1つで、validationやmiddlewareなどのより高度な機能を提供します。まずはいつものようにインストールから。

 > npm install mongoose


Mongooseでは最初にSchemaを定義してdocumentを生成します。以下、サンプルプログラムです。

var mongoose = require('mongoose');

// enum
var genders = 'male,female,neuter'.split(',');

// DBクローズ処理。
var closeDB = function() {
  db.close(function(err, result) {
    console.log("close!");
  });
};

// コネクション。dbオプションとしてsafe:true, serverオプションとしてauto_reconnect:trueを設定。
var opts = {server:{auto_reconnect:true}, db:{safe:true}};
//var db = mongoose.createConnection('mongodb://localhost/exampleDB2', opts);
var db = mongoose.createConnection('localhost', 'exampleDB2', 27017, opts);

// 接続エラーの場合
db.on('error', console.error.bind(console, 'connection open error.'));

// 接続オープン
db.once('open', function() {
    // サンプルスキーマ
    var userSchema  = mongoose.Schema({
        uid : { type: String, required:true, index: {unique:true} },
        name: { first: String, last: String },
        age : { type: Number, min: 1 },
        gender : { type: String, enum: genders }
    });

    // collectionの定義。
    userSchema.set('collection', 'users');

    // モデルの作成。
    var User = db.model('User', userSchema);

    // インスタンス(document)作成。
    User.create(
      { uid: 'kat',
        name: {first: 'KAT', last:'INTINK'},
        age: 100,
        gender: 'male'
      }, function(err, user) {
        if (err) {
          console.log('[error] create');
          console.log(err);
          closeDB();
        } else {
          console.log('create done.');

          // ドキュメントの検索。
          User.find().exec(function(err, user) {
            if (err) {
              console.log('[error] query');
              console.log(err);
              closeDB();
            } else {
              console.log('query done.');
              console.log(user);

              // ドキュメントの削除。
              User.remove(function(err) {
                if (err) {
                  console.log('[error] remove');
                  console.log(err);
                  closeDB();
                } else {
                  console.log('remove done.');
                  closeDB();
                }
              });
            }
          });
        }
    });
});


Node.jsが面白い件③ MongoDBで遊ぶ 〜基本編〜で説明したNative Driverを使った方法と違い、Modelを通して処理をしていることがわかります。簡単/単純な処理を記述する場合はNative Driverでも十分ですが、世の中のODMと同じようにMongooseにも便利な機能が実装されています。次はそれらの機能について説明してみます。

2012年11月24日土曜日

Node.jsが面白い件③ MongoDBで遊ぶ 〜基本編〜

今回はMongoDBの使い方についてです。MongoDBは有名なNoSQLの1つで、Redisと同じようにNode.jsでも簡単に使えます。MongoDB Native NodeJS Driverをダウンロードし、こちらのマニュアルに沿って記述すれば基本的な使い方はO.K.です! ・・・で終わると書いている意味もないので、ここでは基本的な使い方を記述して行きます。 まずはいつも通りnpmでインストール!
 > npm install mongodb

続いて、データベースに接続してコレクションを作成した後ドキュメントを挿入し、コレクションを削除するという簡単なサンプルプログラム。
var mongo = require('mongodb');

// ドキュメントサンプル。
var doc1 = {'key1':'val1'};
var doc2 = {'key2-1':'val2-1', 'key2-2':'val2-2'};
var docs = [doc1, doc2];

// serverの設定。
var server = new mongo.Server('localhost', 27017, {auto_reconnect: true});

// dbの設定。
var db = new mongo.Db('exampleDb', server, {safe:true});
//var db = new mongo.Db('exampleDb', server);

var closeDB = function() {
    db.close(true, function(err, result) {
        console.log("close!");
    });
}

// コネクションオープン。
db.open(function(err, db) {
    if (!err) {
        console.log("open!");
        // コレクションの作成。既に存在する場合はエラー。
        db.createCollection('collection1', {strict:true}, function(cerr, collection) {
            if (!cerr) {
                console.log("create!");

                // ドキュメントの格納。
                collection.insert(docs, function(ierr, res) {
                    if (!ierr) {
                        console.log("insert doc1!");

                        // ドキュメントの検索。
                        collection.find().toArray(function(ferr, items) {
                            if (!ferr) {
                                console.log(items);

                                // コレクションの削除。
                                collection.drop();
                                closeDB();
                            } else {
                                console.log(ferr);

                                collection.drop();
                                closeDB();
                            }
                        });
                    } else {
                        console.log(ierr);

                        collection.drop();
                        closeDB();
                    }
                });
            } else {
                console.log(cerr);

                closeDB();
            }
        });
    } else {
        console.log(err);
    }
});

MongoDBでは、今のところデフォルトで書き込みの結果を待たずにリターンを返します(safe=false)。つまり、書き込みが成功したか失敗したかがすぐにわかりません。これは、データベースを作成する際にsafe=trueを設定することで変更できます。また、書き込みの挙動は以下のように設定することができます。
  1. j:ジャーナル(redoログのようなもの)のコミットを待つ。ジャーナルが設定されていなければそのまま返す。
  2. w:レプリカに指定数反映されるのを待つ。タイムアウト指定も可能。
  3. fsync:ジャーナリングされてない場合は全てのファイルをfsyncするのを待つ。ジャーナリングしている場合は次のグループのコミットを待つ。非推奨なのでjを使うべき。

MongoDBでは動的にcollectionを作成するケースも多いですが、既に作成しようとするcollection存在する場合はエラーを受け取りたい、という場合もあります。その場合はcreateCollectionでstrict:trueを指定します。
  1. createCollectionでstrict:false(デフォルト)の場合、既に存在する場合は何もしない(作成しない)
  2. createCollectionでstrict:trueの場合、既に存在する場合はエラーを返す。
  3. collectionでstrict:false(デフォルト)の場合、コレクションが存在しない場合は最初のinsertで作成する。
  4. collectionでstrict:trueの場合、コレクションが存在しない場合はエラーを返す。
サンプルプログラム内ではinsert/findを実施しています。この辺りはupdateやdeleteも含めて多くの処理がありますので、ドキュメントを見ながら実装していけばよいと思います。
Node.jsというよりMongoDBの話になりましたが、他の言語でMongoDBを使っている開発者ならかなり違和感なく使えそうですね。また、MongoDBはWeb上にもドキュメントが揃ってきていますので、NoSQL初心者でも比較的情報は得やすいですね。