タグ : Mongoose

Mongoose documents を JSON に変換して response を返す

Express.js + Mongoose で documents を JSON に変換して response を返すには、

UserModel.find().lean().exec(function (err, users) {
  return res.json(users)
});

というような感じで lean() オプションを付けて plain javascript object を取得した値を res.json で返せばよいです。


参考情報

[Mongoose] index が未作成な field を sort に指定して stream で取得すると default batchSize だけ処理して正常終了するので困った

Mongoose で index が作成されていない field を sort に指定して stream で取得すると、 default batchSize の 1000 件だけ処理して正常終了するので困ったというお話です。

find() で全件一括取得するケース

find() で全件一括取得すると callback の第一引数 err には値は入らず、配列の末尾の要素に $err, code が含まれていました。

User.find().sort({
  createdAt: -1
}).setOptions({
  noCursorTimeout: true
}).exec(function(err, docs) {
  console.log(err);
  console.log(docs.length);
  console.log(docs[docs.length - 1]);
 
/**
以下、console.log の出力結果
 
null
1001
{ '$err': 'getMore runner error: Overflow sort stage buffered data usage of 33555763 bytes exceeds internal limit of 33554432 bytes',
  code: 17406,
...
**/
});

stream() で全件取得するケース

stram() で取得する場合も同じで、最後に取得した document に $err, code が含まれていました。

var stream = User.find()
.sort({ createdAt: -1 })
.setOptions({ noCursorTimeout: true })
.stream();
 
stream.on('data', function(user){
/**
default batchSize 1000 件だけ取得して、1001 件目に $err, code を含むデータを取得して、正常終了する
{ '$err': 'getMore runner error: Overflow sort stage buffered data usage of 33555763 bytes exceeds internal limit of 33554432 bytes',
  code: 17406,
...
**/
});
 
stream.on('error', function(err){
  // エラーイベントは呼び出されない
});

解決方法

sort に指定する field の index を作成する

index が作成されていないのが元々の問題なので、index 作成すれば解決します。

User.path('createdAt').index(true);

index が存在する field を sort に指定する

index size が気になるので作成したくないという場合は、default で用意されている _id を sort に指定する対応でもいいと思います。

User.find().sort({
  _id: -1
}).setOptions({
  noCursorTimeout: true
}).exec(function(err, docs) {});

所感

document が 1000 件しか取得できてないことに気付かない限り、エラーが発生していることに気付けないので Mongoose のエラーハンドリングの実装が微妙なのでは…


参考情報

[Node.js] Mongoose の virtual attributes を sinon で stub 化する

Mongoose の virtual attributes を sinon で stub 化する方法をご紹介します。

まず、下記のような Product model が定義されているとします。

# define Mongoose Product model
mongoose = require 'mongoose'
 
productSchema = new mongoose.Schema({
  # something
})
 
productSchema.virtual('discountPercentage').get ()->
  # calculate discountPercentage
  return discountPercentage
 
Product = mongoose.model 'Product', productSchema

そして、テストコードで discountPercentage virtual をスタブ化するコードは下記のような感じです。

# in test code
sinon = require 'sinon'
 
product = new Product()
 
console.log product.discountPercentage
# discountPercentage virtual の計算結果を返す
 
stub = sinon.stub product, 'get'
stub.withArgs('discountPercentage').returns 50
 
console.log product.discountPercentage
# 必ず 50 を返す
 
# 使い終わったら、忘れずに restore しましょう
stub.restore()

ポイントは sinon.stub mongooseInstance, ‘get’stub.withArgs(‘virtualName’).returns の部分で virtualName の返り値をスタブ化しているという点だけです。


参考情報

[Node.js] MONGOOSE WARNING がでないように stable version を使おう

Node.js + Mongoose のアプリ開発で MONGOOSE WARNING という警告メッセージが表示されたら、 unstable バージョンを利用していると思われるので stable なバージョンを使いましょう。

まず、アプリを起動しようとすると !!! MONGOOSE WARNING !!! と派手に怒られます。

$ node-dev app.js     
 
##############################################################
#
#   !!! MONGOOSE WARNING !!!
#
#   This is an UNSTABLE release of Mongoose.
#   Unstable releases are available for preview/testing only.
#   DO NOT run this in production.
#
##############################################################
 
Error: Cannot find module 'knox'
    at Function.Module._resolveFilename (module.js:338:15)
    at Function.Module._load (module.js:280:25)
    at Module.require (module.js:364:17)
    at require (module.js:380:17)
    at Object.<anonymous> (/Users/bakorer/works/myapp/routes/admin/content.js:5:12)
    at Module._compile (module.js:456:26)
    at Module._extensions..js (module.js:474:10)
    at Object.nodeDevHook [as .js] (/Users/bakorer/.nvm/v0.10.28/lib/node_modules/node-dev/lib/hook.js:43:7)
    at Module.load (module.js:356:32)
    at Function.Module._load (module.js:312:12)
[ERROR] 10:45:41 Error

package.json に “mongoose”: “^3.8.13”, と記述されていたので npm install したら Mongoose version 3.9.7 がインストールされておりました。

$ npm list | grep mongoose
├─┬ mongoose@3.9.7 invalid
├─┬ mongoose-file@0.0.2
├─┬ mongoose-paginate@3.1.5
│ └─┬ mongoose@4.0.6
├── mongoose-query-paginate@1.0.1
npm ERR! invalid: mongoose@3.9.7 /Users/bakorer/myapp/node_modules/mongoose
npm ERR! not ok code 0

Mongoose の version を確認して使えそうな stable version に package.json を書き換えて、再度 npm install すれば OK なはず。

$ git diff
 
diff --git a/package.json b/package.json
index e56cf62..6b49881 100644
--- a/package.json
+++ b/package.json
@@ -15,7 +15,7 @@
-    "mongoose": "^3.8.13",
+    "mongoose": "3.8.13",

package.json の書き方は下記のサイトが詳しいので、あわせて読んでおくといいかもです。


参考情報

[Mongoose] lean option を有効にすると MongooseDocument ではなく plain javascript object が返ってくる

タイトルで完結していますが、Mongoose で lean option を有効にすると MongooseDocument ではなく plain javascript object が返ってきます。

Documents returned from queries with the lean option enabled are plain javascript objects, not MongooseDocuments. They have no save method, getters/setters or other Mongoose magic applied.

Mongoose ドキュメントではないので、save メソッドや virtual の getter, setter や instance メソッドなどなど Mongoose にしかない便利機能は使えなくなります。

(例) lean option を使った Query

new Query().lean() // true
new Query().lean(true)
new Query().lean(false)
 
Model.find().lean().exec(function (err, docs) {
  docs[0] instanceof mongoose.Document // false
});

業務で lean option を有効にしたときに困ったことがありまして、populate した document の virtual を使っている箇所があり、その部分が動かなくなってしまったことです。

改修方針としては virutal を使わないで、plain js object を引数として渡して使う method を実装して、それを使うように修正しました。

パフォーマンスチューニングをするような状況にならないと lean option を使うことはなさそうですが、そのときは Mongoose の機能を利用した既存処理が動かなくなっていないか注意してみるといいかもしれません。

[Mongoose] 途中から field に default 定義を追加する場合の実装方針

Mongoose で途中から field に default 定義を追加する場合の実装方針を忘れないように書き残しておきます。

var mongoose = require('mongoose');
 
var orderSchema = new mongoose.Schema({
  // 合計金額
  amount: {
    type: Number
  },
  // 送料無料の条件が適用される閾値 (単位: USD)
  freeShippingThreshold: {
    type: Number,
    default: 100
  }
});
 
var Order = mongoose.model('Order', orderSchema);
 
 
/**
 * Case1: 新規 instance
 * instance 化されたときはデフォルト値が入った状態で便利
 **/
var order = new Order();
/**
 * order : {
 *    _id: 5512abca1c6bb890a82b0951,
 *    freeShippingThreshold: 100
 * }
 **/
 
 
/**
 * Case2: MongoDB に保存されている既存データから取得した instance
 *
 * MongoDB に保存されているデータは下記の通り
 * 
 * order : {
 *   _id: 50169294b1b4a1c21e00001e,
 *   amount: 150
 * }
 **/
Order.findOne(function (err, order) {
  /**
   * Mongoose の findOne method で instance 取得したときは、
   * 値が入ってなければ default に設定した値がセットされる
   * 
   * order : {
   *   _id: 50169294b1b4a1c21e00001e,
   *   amount: 150,
   *   freeShippingThreshold: 100
   * }
   **/
});

Case1 の新規インスタンスを作成するケースは、デフォルトで freeShippingThreshold に 100 が入ってるので、使い回しが便利です。何の問題もなさそうですね。

Case2 は既に MongoDB に保存されているが freeShippingThreshold に値が入ってないので、 100 が入った状態で order インスタンスを取得できます。しかし、この order が保存されたときはキャンペーン期間中で送料無料の閾値が $50 になっていたので 50 が入ったデータが欲しいわけです。こういうケースは後から default: 100 を設定するタイミングで既存データに適切な freeShippingThreshold を設定してあげる必要があるわけです。

以上、うっかりしてると陥るかもしれない Mongoose の運用事例でした。

[Mongoose] ObjectId のバリデーションには mongoose.Types.ObjectId.isValid を使おう

Mongoose で ObjectId のバリデーションをするには mongoose.Types.ObjectId.isValid というメソッドがあるのでこれを使いましょう。

var mongoose = require('mongoose');
 
console.log(mongoose.Types.ObjectId.isValid);
// [Function: isValid]
 
console.log(mongoose.Types.ObjectId.isValid('53cb6b9b4f4ddef1ad47f943'));
// true
 
console.log(mongoose.Types.ObjectId.isValid('spam'));
// false

参考情報

Best way to validate an ObjectId · Issue #1959 · LearnBoost/mongoose

[Mongoose] 特定の field が変更されたかは isDirectModified, isModified, modifiedPaths で確認できる

Mongoose では document の特定の field に変更があったかどうかを isDirectModified や isModified, modifiedPaths などの method で確認できます。

Ruby on Rails の ActiveRecord でいうところの changed? や 属性名_changed? と似たような機能が Mongoose でも提供されています。

では、1つずつ紹介していきましょう。

Document#isDirectModified(path)

isDirectModified メソッドは引数に指定した field 自身が変更されていれば true をそうでなければ false を返します。

doc.set('documents.0.title', 'changed');
 
// 'documents.0.title' 自体が変更されているので true を返す
doc.isDirectModified('documents.0.title') // true
 
// 'documents' の下の階層で変更されているが 'documents' 自体は変更されていないので false を返す
doc.isDirectModified('documents') // false

Document#isModified([path])

isModified メソッドは引数に指定した field に関係がある箇所が変更されていれば true をそうでなければ false を返します。

doc.set('documents.0.title', 'changed');
 
// 変更された field とそれを含む上の階層は true を返す
doc.isModified()                    // true
doc.isModified('documents')         // true
doc.isModified('documents.0.title') // true
 
// 変更されてない field を指定すると false を返す
doc.isModified('something')         // false
 
// 'documents' の下の階層で変更されているが 'documents' 自体は変更されていないので false を返す
doc.isDirectModified('documents')   // false

Document#modifiedPaths()

modifiedPaths メソッドは変更があった field 名を Array で返します。

doc.set('name', 'test name');
doc.set('documents.0.title', 'changed');
 
doc.modifiedPaths()
[ 'name',
  'documents.0.title' ]

これらの機能をうまく使ってロジックをすっきり書けるといいですね。

[Mongoose] TypeError: Cannot read property ‘options’ of undefined

Mongoose でスキーマ定義に type: ObjectId としている field にオブジェクト型のデータが入ってると TypeError: Cannot read property ‘options’ of undefined エラーが発生します。

TypeError: Cannot read property 'options' of undefined
  at ObjectId.cast (/u/apps/com/shared/node_modules/mongoose/lib/schema/objectid.js:99:22)
  at /u/apps/com/shared/node_modules/mongoose/lib/document.js:288:29
  at model.Document.$__try (/u/apps/com/shared/node_modules/mongoose/lib/document.js:769:8)
  at init (/u/apps/com/shared/node_modules/mongoose/lib/document.js:287:16)
  at model.Document.init (/u/apps/com/shared/node_modules/mongoose/lib/document.js:246:3)
  at completeOne (/u/apps/com/shared/node_modules/mongoose/lib/query.js:1392:10)
  at Promise.<anonymous> (/u/apps/com/shared/node_modules/mongoose/lib/query.js:1160:11)
  at Promise.<anonymous> (/u/apps/com/shared/node_modules/mongoose/node_modules/mpromise/lib/promise.js:177:8)
  at Promise.EventEmitter.emit (events.js:95:17)
  at Promise.emit (/u/apps/com/shared/node_modules/mongoose/node_modules/mpromise/lib/promise.js:84:38)
  at Promise.fulfill (/u/apps/com/shared/node_modules/mongoose/node_modules/mpromise/lib/promise.js:97:20)
  at Promise.resolve (/u/apps/com/shared/node_modules/mongoose/lib/promise.js:114:23)
  at /u/apps/com/shared/node_modules/mongoose/lib/model.js:2029:23
  at process._tickCallback (node.js:415:13)

再現手順ですが、例えば下記のようなスキーマ定義で、

Article = new Schema
  user:
    type: ObjectId
    ref: 'User'

user filed に ObjectId ではなく { name : ‘hoge’ } のような値を入れて、save するとエラーが発生します。

article = new Article
article.user = { name : 'hoge' } // ObjectId じゃない!
article.save()->

findAndModify などの Mongo DB Native NodeJS Driver を直接呼ぶメソッド findOneAndUpdate メソッドを使ったり、mongo shell で直接データを編集したりするとこういうエンバグさせてしまうので、必要ない限りやめたいですね。


参考情報

findOneAndUpdate – Mongoose API v3.8.18

findAndModify — MongoDB Manual 2.6.4

[Mongoose] stream を使ってバッチ処理するときは noCursorTimeout: true オプションを設定すると幸せになれるかも

Express.js(Node.js) + Mongoose(MongoDB) という構成で、バッチ処理を長時間実行すると途中で終了してしまう問題が発生しました。

状況としては、まだ cursor が次のデータを取得できるはずなのに、stream.on ‘data’ 内では次のデータが渡ってこなくて、stream.on ‘error’ が呼ばれることなく、stream.on ‘close’ が呼ばれている感じです。cursor がタイムアウトしてしまっているのが原因らしいです。

解決方法としては、第三引数に noCursorTimeout: true を指定することで、途中で終了せずにバッチ処理を最後まで実行することができました。

var stream = User.find(
  {},
  {},
  { noCursorTimeout: true }
).stream();
 
stream.on('data', function (doc) {
  // do something with the mongoose document
}).on('error', function (err) {
  // handle the error
}).on('close', function () {
  // the stream is closed
});

参考情報