タグ : mongodb

[MongoDB] データベース名の変更方法

MongoDB でデータベース名を変更する手順をご紹介します。

MongoDB

前提

まず、MongoDB には DB 名をアトミックにリネームする方法はないので、コピーして古い DB を削除するという手順になります。

DB名の変更手順

以下、MongoDB でデータベース名を変更するコマンドです。

// データベースを変更したい名前をつけてコピーする
db.copyDatabase('old_name', 'new_name');
// 古いデータベースに切り替える
use old_name
// 古いデータベースを削除する
db.dropDatabase();

データベースの容量が大きいと db.copyDatabase に時間が掛かってしまいますが、気長に待つしかなさそうです。

[MongoDB] field 名の命名規則

MongoDB のドキュメントを確認すると field の命名規則にゆらぎがあるので決めの問題なんでしょうね。

MongoDB

places_id と snake_case で書かれています。

こちらは cancelDate と lowerCamelCase で書かれています。

アプリケーションコードで扱いやすい命名規則に書き方を統一するといいんじゃないでしょうか。

[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 のエラーハンドリングの実装が微妙なのでは…


参考情報

[MongoDB] E11000 duplicate key error index の code 11000 と 11001 の違い

MongoDB version 2.4.5 で unique index による duplicate key error index エラー発生時の error code には 11000 と 11001 があるのですが、その違いについて調べてみました。

MongoDB

事前準備

> db.version()
2.4.5
 
> db.so.drop();
> db.so.insert( { foo: 5 } );
> db.so.ensureIndex( { foo: 1 }, { unique: true } );
> db.so.insert( { foo: 6 } );

新規追加系メソッド insert のケース

db.collection.insert() で duplicate key error index エラーが発生したときは 11000 のエラーコードを返します。

> db.so.insert( { foo: 5 } );
E11000 duplicate key error index: test.so.$foo_1  dup key: { : 5.0 }
 
> db.getPrevError();
{
	"err" : "E11000 duplicate key error index: test.so.$foo_1  dup key: { : 5.0 }",
	"code" : 11000,
	"n" : 0,
	"nPrev" : 1,
	"ok" : 1
}

更新系メソッド update / save のケース

db.collection.update()db.collection.save() などの更新系のメソッドで duplicate key error index エラーが発生したときは 11001 のエラーコードを返します。

このときに紛らわしいのは、エラーメッセージ E11000 duplicate key error index に含まれているエラーコードが 11000 ということです。

> db.so.update( { foo: 6 }, { $set: { foo: 5 } } );
E11000 duplicate key error index: test.so.$foo_1  dup key: { : 5.0 }
 
> db.getPrevError();
{
	"err" : "E11000 duplicate key error index: test.so.$foo_1  dup key: { : 5.0 }",
	"code" : 11001,
	"n" : 0,
	"nPrev" : 1,
	"ok" : 1
}
> var so6 = db.so.findOne( { foo: 6 } )
> so6.foo = 5
5
 
> db.so.save( so6 )
E11000 duplicate key error index: test.so.$foo_1  dup key: { : 5.0 }
 
> db.getPrevError();
{
	"err" : "E11000 duplicate key error index: test.so.$foo_1  dup key: { : 5.0 }",
	"code" : 11001,
	"n" : 0,
	"nPrev" : 1,
	"ok" : 1
}

まとめ

MongoDB version 2.4.5 では、エラーメッセージは E11000 duplicate key error index と同じだけど、エラーコードは 11000 と 11001 の2種類があるバグっぽい処理になっていました。

このため、プログラム上で duplicate key error を処理したいときは if(err.code === 11000 || err.code === 11001) みたいな感じで扱うといいかもしれません。

なお MongoDB version 3.0.2 で試してみたところエラーコードは 11000 に統一されているようでした。

なので、err.code === 11001 だけのコードは MongoBD を upgrade したタイミングで動かなくなってしまうので注意が必要そうです。

> db.version()
3.0.2
 
> db.so.insert( { foo: 5 } );
WriteResult({
	"nInserted" : 0,
	"writeError" : {
		"code" : 11000,
		"errmsg" : "E11000 duplicate key error index: test.so.$foo_1 dup key: { : 5.0 }"
	}
})
 
> db.so.update( { foo: 6 }, { $set: { foo: 5 } } );
WriteResult({
	"nMatched" : 0,
	"nUpserted" : 0,
	"nModified" : 0,
	"writeError" : {
		"code" : 11000,
		"errmsg" : "E11000 duplicate key error index: test.so.$foo_1 dup key: { : 5.0 }"
	}
})
 
> var so6 = db.so.findOne( { foo: 6 } )
> so6.foo = 5
5
> db.so.save( so6 )
WriteResult({
	"nMatched" : 0,
	"nUpserted" : 0,
	"nModified" : 0,
	"writeError" : {
		"code" : 11000,
		"errmsg" : "E11000 duplicate key error index: test.so.$foo_1 dup key: { : 5.0 }"
	}
})

以上です。

参考情報

[MongoDB] db.runCommand( { cloneCollection } ) の使い方

MongoDB で collection をコピーする db.runCommand( { cloneCollection } ) の使い方をご紹介します。

MongoDB

host: targethost1, port: 27017 と host: currenthost, port: 27018 で mongod instance が2つ立ち上がっている状態とします。

以下のようなクエリで targethost1:27017 の targetDb database にある users collection から verified_at が null ではない document を currentDb に同じ collection 名でコピーできます。

mongo --host currenthost --port 27018 currentDb
 
var query = { verified_at : { $ne : null } };
db.runCommand( { cloneCollection: 'targetDb.users', from: 'targethost1:27017', query : query } );

コピーした collection 名を変更したい場合は renameCollection でできます。

以上です。

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

[MongoDB] Array の length で sort するために aggregate を使う

MongoDB で Array type な field を length で sort して取得するサンプルクエリをご紹介します。

MongoDB

MongoDB Query

db.posts.aggregate(
[
  { $unwind: "$likes" },
  { $group: {
      _id: "$_id",
      size: { $sum: 1 }
    }
  },
  { $sort: { size: -1 } }
])

Results

{ "_id" : ObjectId("514e23e06aeaa880410321aa"), "size" : 590 }
{ "_id" : ObjectId("545c796972b4c28015ab4eac"), "size" : 395 }
{ "_id" : ObjectId("51440f7d4ca4c41367001dff"), "size" : 321 }
{ "_id" : ObjectId("51d6eef05204c9046100031d"), "size" : 315 }
{ "_id" : ObjectId("54ae81f1f158d13d60c50116"), "size" : 271 }
//...

参考情報

[MongoDB] 銀行口座のスキーマ定義

以前、Mongoose (MongoDB) で銀行口座の情報を取り扱う Scheme の定義をしたときに悩んで付けた field 名を公開します。

MongoDB

BANK_ACCOUNT_TYPE =
  SAVINGS: 1
  CURRENT: 2
 
BANK_ACCOUNT_TYPE_KEYS = Object.keys(BANK_ACCOUNT_TYPE)
BANK_ACCOUNT_TYPE_PATTERN = BANK_ACCOUNT_TYPE_KEYS.map (k)-> BANK_ACCOUNT_TYPE[k]
 
HogeScheme = new Scheme
bank_account:
  bank_name:
    type: String
  bank_code:
    type: String
  branch_name:
    type: String
  branch_code:
    type: String
  account_type:
    type: Number
    enum: BANK_ACCOUNT_TYPE_PATTERN
  account_number:
    type: Number
  account_holder_name:
    type: String
  account_holder_name_kana:
    type: String

というような感じにしました。

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

参考情報