Mongoose のモデルに独自メソッドを追加する

久しぶりに Mongoose ネタです.Schema の API である static メソッドについて少しだけ.
今回は短め.

見出し

  • 何ができる?
  • 登録した関数内での処理
  • 実際の定義
  • 参考にした情報

何ができる?

例えば User というモデルを定義 (UserSchema) したとします.User に対しては,find/findOne といったメソッドを呼び出すことができます.
しかし,例えば DataMapper のような「first_or_create (あれば最初の 1 つを返す,無ければ作って返す)」が欲しくなった場合はどうすればよいでしょう?
上記のような場合に Schema.static メソッドを使用します.Schema.static メソッドは,モデルに対して独自の処理を追加することができます.

var UserSchema = new Schema({
  name: String,
  created_at: { type: Date, default: Date.now }
});

UserSchema.static('first_or_create', function(query, callback) {
  // ここに処理を定義
});

// Schema を登録
mongoose.model('User', UserSchema);

// DB に接続してモデルを取得
var db = mongoose.createConnection('...');
var User = db.model('User');

// first_or_create が呼び出せる
User.first_or_create({ name: 'krdlab' }, function(err, user) {
  // ...
});

登録した関数内での処理

User.first_or_create と呼び出すことから,当然 this はモデル (User) を指しています.このため,関数内の処理で 'this.find' と呼び出せば,接続先 DB が解決された状態で操作を呼び出せます.
Schema に登録したメソッドがモデルに定義されるのは,Schema からモデルを生成するとき (mongoose.model に Schema を渡したとき) です.以下のコードから Schema.static で登録した関数をモデルのプロパティに登録していることがわかります.

// lib/mongoose/index.js:149
Mongoose.prototype.model = function (name, schema, collection, skipInit) {
  // ... 省略

  if (!this.models[collection][name]) {
    model = Model.compile(name
                        , this.modelSchemas[name]
                        , collection
                        , conn
                        , this);  // ← ここ★

    if (!skipInit) model.init();

    this.models[collection][name] = model;
  }

  return this.models[collection][name];
};
// lib/mongoose/model.js:648
Model.compile = function (name, schema, collectionName, connection, base) {
  // ... 省略

  // apply methods
  for (var i in schema.methods)
    model.prototype[i] = schema.methods[i];

  // apply statics
  for (var i in schema.statics)
    model[i] = schema.statics[i];    // ← ここ★

  // ... 省略

  return model;
};

これで,mongoose.model('User') から返ってくる User に first_or_created が定義されます.

実際の定義

上記を踏まえると,以下のようになります.

var UserSchema = new Schema({
  name:       String,
  created_at: { type: Date, default: Date.now },
});

UserSchema.static('first_or_create', function(query, callback) {
  var U = this;
  U.findOne({ name: query.name }, function(err, user) {
    if (err) {
      callback(err);
    } else if (user) {
      callback(null, user);
    } else {
      var newU = new U({ name: query.name });
      newU.save(function(err) {
        if (err) {
          callback(err);
        } else {
          callback(null, newU);
        }
      });
    }
  });
});

mongoose.model('User', UserSchema);

実際には,登録する関数を function(query, defaults, callback) として,新規作成時のプロパティ値を指定できるとなお良いでしょう.