Tokyo Otaku ModeではNode.jsからMongoDBにアクセスするのにODMとしてMongooseを採用しています。
Mongoose(ODM)を利用するメリットとしては、
- collectionのSchema設計がコードに残る
- virtualなどデータ周りの機能がModelに集約できる
- populationが利用できる
などが上げられます。
どれもサービスを効率的に作る上で助かる機能ばかりですが、とくにpopulationは別ドキュメントのreference(_id
)を持っているだけで、自動的にドキュメントに展開してくれる強力な機能です。
一方で、DBロック回避の目的やある程度の規模になってDBを分割するようになると、別DBのcollectionをpopulateできないという問題がでてきます。
Mongooseの仕様上、特定DBへのconnectionとModel(collection)が密に結びついているため、参照元のModelと同じconnectionを持っていないと、参照先に辿りつけないからです。
これを回避するため、Tokyo Otaku ModeではMongooseのconnectionに少し手を入れてDB間のpopulationを実現しています。
まず簡単にpopulationの機能を説明します。
以下の様な、テキストが保存されたPostsドキュメントとそのテキストを投稿したユーザーが保存されたUsersドキュメントがあるとします。
Posts.js 投稿データ1 2 3 4 5
| { "_id" : ObjectID("540805939d1960ae87ab0149"), "user": ObjectId("540805ad9d1960ae87ab014a"), "text": "Message" }
|
Users.js ユーザーデータ1 2 3 4
| { "_id" : ObjectID("540805ad9d1960ae87ab014a"), "name": "Test User" }
|
こういうドキュメント構造に対して、populationを指定して実行すると、
1 2 3
| Posts.findById("540805939d1960ae87ab0149") .populate("user") .exec(callback);
|
userが展開されて取得できます。
Posts.js 投稿データ1 2 3 4 5 6 7 8
| { "_id" : ObjectID("540805939d1960ae87ab0149"), "user": { "_id" : ObjectID("540805ad9d1960ae87ab014a"), "name": "Test User" }, "text": "Message" }
|
裏側では、populationで指定されたフィールド(user)のリファレンス先collection(Users)に接続してデータを取得・展開しているだけですが、自分自身と同じconnectionを利用するので、別DBのcollectionは未定義のModelと判断されてしまいます。
さて、では実際にどう解決しているのかコードベースで解説いたします。
設定情報
環境ごとの設定ファイルになります。
ModelがどのDBに存在するかのマッピングを設定するroutesと、それぞれの接続情報が書かれています。何も設定しないとmongodb1
DBに接続するようになっています。
conf/mongodb.coffee1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83
|
module.exports = masterName: "mongodb1" servers: mongodb1: replicaSet: members: [ host: "mongodb1a" port: 27017 database: "tokyootakumode" username: "username" password: "password" , host: "mongodb1b" port: 27017 database: "tokyootakumode" username: "username" password: "password" , host: "arbiter1" port: 27017 database: "tokyootakumode" username: "username" password: "password" ] options: replset: rs_name: "mongodb1" db: slave_ok: true
mongodb2: replicaSet: members: [ host: "mongodb2a" port: 27017 database: "tokyootakumode" username: "username" password: "password" , host: "mongodb2b" port: 27017 database: "tokyootakumode" username: "username" password: "password" , host: "arbiter2" port: 27018 database: "tokyootakumode" username: "username" password: "password" ] options: replset: rs_name: "mongodb2" db: slave_ok: true
mongodb3: host: "mongodb3" port: 27017 database: "tokyootakumode" username: "username" password: "password"
routes: Users: "mongodb1" Posts: "mongodb2" Rankings: "mongod3"
|
コネクションマネージャ
上記設定情報を元に、どのModelをどのconnectionに結びつけるかを決定し、実際にコネクションを管理します。
肝は最初の接続時に接続先connectionとModel情報をマッピングしておき、Modelを呼び出す際に、対象のconnectionに書き換える点です。
document/manager.js1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123
| var mongoose = require('mongoose'); var conf = require(__root + '/conf/mongodb'); var Connection = require('mongoose/lib/connection'); var Model = require('mongoose/lib/model'); var Schema = require('mongoose/lib/schema');
var manager = {
connections: {}, paths: {},
dispatch: function(name, schema) { if (conf.routes && conf.routes[name]) { if (!(this.connections[conf.routes[name]] instanceof Connection)) { throw new Error('"' + conf.routes[name] + '" is not Mongoose connection.'); } return this.connections[conf.routes[name]].model(name, schema); }
return this.connections[conf.masterName].model(name, schema); } };
var routes = {}; var connections = [];
Connection.prototype.originalModel = Connection.prototype.model; Connection.prototype.model = function(name, schema, collection) {
if (schema instanceof Schema) { if (this.options && this.options.replset && this.options.replset.rs_name) { routes[name] = this.options.replset.rs_name + '/' + this.name; } else { routes[name] = this.host + ':' + this.port + '/' + this.name; } return Connection.prototype.originalModel.call( this, name, schema, collection ); }
var dbName = manager.paths[routes[name]]; var connection = manager.connections[dbName]; return Connection.prototype.originalModel.call( connection, name, schema, collection ); }
for (var i in conf.servers) { var server = conf.servers[i];
if (server.replicaSet) { var options = server.replicaSet.options || {}; var uris = []; server.replicaSet.members.forEach(function(member) { uris.push('mongodb://' + member.username + ':' + member.password + '@' + member.host + ':' + member.port + '/' + member.database); }); connections.push({ name: i, connection: mongoose.createConnection(uris.join(','), options) });
} else { var options = server.options || {}; connections.push({ name: i, connection: mongoose.createConnection( 'mongodb://' + member.username + ':' + member.password + '@' + server.host + ':' + server.port + '/' + server.database, options) }); } }
connections.forEach(function(connection) { manager.connections[connection.name] = connection.connection;
if (connection.connection.options && connection.connection.options.replset && connection.connection.options.replset.rs_name) { manager.paths[connection.connection.options.replset.rs_name + '/' + connection.connection.name] = connection.name; } else { manager.paths[connection.connection.host + ':' + connection.connection. port + '/' + connection.connection.name] = connection.name; } });
module.exports = manager;
|
各モデル
通常、mongoose.model
に名前とSchemaを渡すところをmanager.dispatch
に渡します。
Posts.js1 2 3 4 5 6 7 8 9 10 11 12 13
| var manager = require(__root + '/document/manager'); var Schema = require('mongoose').Schema var Posts = new Schema({ "user": { "type": Schema.ObjectId, "ref": "Users" } "text": { "type": String } });
module.exports = manager.dispatch('Posts', Posts);
|
Users.js1 2 3 4 5 6 7 8 9
| var manager = require(__root + '/document/manager'); var Schema = require('mongoose').Schema var Users = new Schema({ "name": { "type": String } });
module.exports = manager.dispatch('Users', Users);
|
最後に
このように、managerにconnectionの管理とModelのマッピング機能を一つにまとめ、各Modelでは特に接続先を意識せずにmongoose.model
をmanager.dispatch
に置き換えるだけで、分断されたDB間でもpopulationが利用できるようにしています。
DBの構成変更は設定ファイルを変更するだけでメンテナンスもしやすくなっています。
Tokyo Otaku ModeではNode.js/MongoDBを使い倒したい エンジニアを募集しています。
興味のある方はこちらからご応募ください。