カテゴリー : Node.js

[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 の機能を利用した既存処理が動かなくなっていないか注意してみるといいかもしれません。

[Jade] mixin の命名規則

Jade には mixin という template を module 化する機能があります。

+hoge(arg) みたいな感じで method ライクに template を呼び出せるのですが、method みたいなだけあって命名規則はどうなってるんだろうと思って軽く調べてみました。

下記は、Jade のサンプルコードです。

mixin dialog-title(title)
  .dialog
    h1= title
    p stuff

snake_case, lowerCamelCase のいずれかでもなく、ハイフンを区切り文字として使っており dialog-title と定義されていました。

ついでに .jade ファイルについても見てみましょう。

extend-layout.jade とあるようにファイル名についてもハイフンが区切り文字として使われていました。

Jade の mixin をハイフン区切りにしておくと、JavaScript の method の命名規則と衝突しないので grep しやすくて良い感じになりそうですね。

[Node.js] phantom モジュールの addCookie メソッドの引数が変更された

Node.js 向けの PhantomJS wrapper である phantom の addCookie メソッドの引数がバージョン 0.7.1 から 0.7.2 で変更になりました。

そのため addCookie メソッドを使ってる箇所がエラーで動かなくなってしまいました。

phantom stdout: TypeError: incompatible type of argument(s) in call to addCookie(); candidates were
    addCookie(QVariantMap)
 
 
phantom stdout:   /u/apps/myapp/shared/node_modules/phantom/shim.js:5621
  /u/apps/com/shared/node_modules/phantom/shim.js:4526
  /u/apps/com/shared/node_modules/phantom/shim.js:4502
  /u/apps/com/shared/node_modules/phantom/shim.js:4382
  /u/apps/com/shared/node_modules/phantom/shim.js:4370

変更点は、

addCookie: (name, value, domain, cb=->) ->

から

addCookie: (cookie, cb=->) ->

に arguments が変更されているだけです。

なので name, value, domain と個別に引数に渡されていたのを cookie オブジェクトにまとめて渡すように、addCookie メソッドを呼び出しているコードを修正すれば OK です。

詳しくは上記の issue と pull request を読むと分かりますが PhantomJS の API に合わせる方針で修正されたようです。

[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' ]

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

[Node.js] console.log や console.error などの出力の先頭に時刻を付ける log-timestamp

Node.js で console.log や console.error などの出力の先頭に時刻を付ける log-timestamp というモジュールをご紹介します。

動作例

> console.log('Before log-timestamp');
Before log-timestamp
 
> require('log-timestamp');
 
> console.log('After log-timestamp');
[2015-02-03T13:45:13.198Z] After log-timestamp
 
> console.error('Error')
[2015-02-03T14:15:18.093Z] Error

log-timestamp を使うことでエラーの発生時刻 error.log に出力できるので、エラーやバグなどの調査に役立てることができそうです。


参考情報

bahamas10/node-log-timestamp

bahamas10/node-log-prefix

[Node.js] Error: bind EADDRINUSE

Node.js (Express) で forever restart 後に bind EADDRINUSE エラーが頻発する障害が発生しました。

Error: bind EADDRINUSE はそのポートが既に使われてる場合に発生するエラーでして、同じ障害に悩まされた人のために解決方法をメモしておきます。

エラーメッセージ

Error: bind EADDRINUSE
    at errnoException (net.js:904:11)
    at net.js:1084:30
    at Object.12:1 (cluster.js:592:5)
    at handleResponse (cluster.js:171:41)
    at respond (cluster.js:192:5)
    at handleMessage (cluster.js:202:5)
    at process.EventEmitter.emit (events.js:117:20)
    at handleMessage (child_process.js:318:10)
    at child_process.js:392:7
    at process.handleConversion.net.Native.got (child_process.js:91:7)
    at process.<anonymous> (child_process.js:391:13)
    at process.EventEmitter.emit (events.js:117:20)
    at handleMessage (child_process.js:318:10)
    at Pipe.channel.onread (child_process.js:343:11)

まず netstat コマンドで node.js サーバーのプロセスで使うポートが他のプロセスに使われていないか確認します。

$ netstat -nltp | grep 3000
(Not all processes could be identified, non-owned process info
will not be shown, you would have to be root to see it all.)
tcp        0      0 0.0.0.0:3000                0.0.0.0:*                   LISTEN      6653/phantomjs

今回は phantom.js が 3000 番ポートで起動していたようでした。phantom.js は利用しているものの他のポート番号を指定して起動させるようにしていたので 3000 番ポートを使っていた理由は不明です。プロセスを停止させて問題ない場合、 ps aux | grep phantomjs で phantom.js の PID (プロセス番号) を特定して kill コマンドでプロセスを終了させます。

最後に forever や pm2 もしくは node コマンドなどで node サーバーを起動させて、エラーログに Error: bind EADDRINUSE が出力されていないことを確認すれば完了です。


参考情報

Node.jsの開発で `warn – error raised: Error: listen EADDRINUSE` って怒られるときの対応 – Qiita

node.js – Nodejs application Error: bind EADDRINUSE when use pm2 deploy – Stack Overflow

Linux ソケット・プログラミングの 5 つの落とし穴

[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
});

参考情報