古くは「LAMP」に代表されるような技術スタック、JavaScript 界隈では「MEAN」スタックという用語もあるくらい、JavaScript(Node.js) との組み合わせでは MongoDB がよく使われるようです。
NoSQL ではスキーマレスのためデータマイグレーションを気にせず開発することも多いわけですが、例えば一度バージョン1をリリースして既にユーザーがついているシステムを更改するような場合、後方互換を保つためにもデータマイグレーション機構は重要です。
本記事では、Node.js 環境で MongoDB および Mongoose を利用しているようなシステムにデータマイグレーション機構を導入する Tips を紹介します。
データマイグレーション機構とは?
有名処で言えば、Ruby on Rails の ActiveRecord に搭載されている機能を想像するとよいでしょう。そこには以下のような特徴があります。
-
データベーススキーマを時系列に進化(またはロールバック)させることができる
- Rails では rake タスクとして実行できる
-
migration ファイルを決まったスキームで実装できる
-
ActiveRecord のモデルを利用することができる
Node.js の世界でのデータマイグレーション事情
Ruby の世界では Rails, ActiveRecord というデファクトスタンダードが確立されていますが、Node.js の世界でのデファクトのフレームワークは残念ながら Express であり、オールインワンのフレームワークでもフルスタックのフレームワークでもありません。
ORM/ODM にしても、ORM/ODM のデファクトは、RDBMS なら Sequelize、MongoDB なら Mongoose で固まりつつありますが、それぞれデータマイグレーション機構を備えているわけではなく、データマイグレーション機構が欲しい場合に「すぐ使える」状況ではないのが実情です。
そのため、ライブラリ選定から進める必要があります。
ライブラリ選定
npm でそれっぽいものを検索したところ、以下を見つけました。
npm パッケージ | ライセンス | npm weekly downloads 2018.10 時点 |
GitHub stars | 直近1ヶ月でのコミット数 2018.10 時点 |
---|---|---|---|---|
migrate-mongoose | MIT | 1405 | 86 | 0 |
mongoose-migrate | (none) | 506 | 63 | 0 |
migrate-mongo | MIT | 3515 | 97 | 25 |
db-migrate | MIT | 28399 | 1480 | 0 |
db-migrate-mongodb | MIT | 2604 | 17 | 2 |
weekly downloads と GitHub stars を見ると db-migrate がダントツなのですが、こちらは db-migrate-mongodb の親プロジェクトで、データベースの差異を吸収するような抽象化がなされています(子プロジェクトとして、MySQL 版や PostgreSQL 版が存在する)。
こと db-migrate-mongodb のみの数字で言えば、migrate-mongo が若干優勢といった感じです。今回は、直近コミット数で現在も開発が進んでいると見受けられる migrate-mongo を採用することにします。
Let's Try ~npm script の準備~
Rails の rake タスクのように使えるとなにかと便利なので、まず npm script を準備しましょう。
"migrate": "npm run migrate:up",
"migrate:create": "migrate-mongo create -f config/migrate.js -- ",
"migrate:status": "migrate-mongo status -f config/migrate.js",
"migrate:up": "migrate-mongo up -f config/migrate.js",
"migrate:down": "migrate-mongo down -f config/migrate.js",
-f
オプションで設定しているのはコンフィグファイルです。MongoDB への接続設定等が入ると思います。環境変数から接続先を取りたい場合は以下のようなファイルにするとよいでしょう。
/**
* Configuration file for migrate-mongo
* @see https://github.com/seppevs/migrate-mongo
*
* @author Yuki Takei
*/
function getMongoUri(env) {
return env.MONGODB_URI ||
env.MONGO_URI ||
((env.NODE_ENV === 'test') ? 'mongodb://localhost/myprj_test' : 'mongodb://localhost/myprj');
}
const mongoUri = getMongoUri(process.env);
const match = mongoUri.match(/^(.+)\/([^/]+)$/);
module.exports = {
mongoUri,
mongodb: {
url: match[1],
databaseName: match[2],
options: {
useNewUrlParser: true, // removes a deprecation warning when connecting
},
}
};
Let's Try ~初めての migration ファイル作成~
npm script を叩きましょう。
npm run migrate:create my-first-migration
Let's Try ~migration の実装1~
まずは Mongoose 対応の migration ファイルの書き方です。
my-first-migration.js
'use strict';
require('module-alias/register');
const logger = require('@alias/logger')('growi:migrate:my-first-migration');
const mongoose = require('mongoose');
const config = require('@root/config/migrate');
module.exports = {
async up(db) {
logger.info('Apply migration');
mongoose.connect(config.mongoUri, config.mongodb.options);
const User = require('@server/models/user');
...
logger.info('Migration has successfully applied');
},
async down(db) {
logger.info('Undo migration');
mongoose.connect(config.mongoUri, config.mongodb.options);
const User = require('@server/models/user');
...
logger.info('Migration has successfully undoed');
}
};
@alias
や @root
、 @server
等は、module-alias パッケージで設定したエイリアスパスです。ご自身の環境に合わせて読み替えてください。
コード解説
up
function が migration の適用のためのコード、 down
function がロールバックのためのコードになります。それぞれ ES6 で記述可能な async function として定義しています。
User
インスタンスは、 mongoose.model()
で作成されるモデルインスタンスという想定です。
それぞれの function の2行目の mongoose.connect()
を呼ばなければ Mongoose のモデルインスタンスのメソッドを呼んでも MongoDB にアクセスされませんので注意してください。
Let's Try ~migration の実装2~
次に、 MongoDB Driver を直に利用するような Example を紹介します。例えば既存の Mongoose モデルを廃棄するような場合です。
abolish-myrelations.js
'use strict';
require('module-alias/register');
const logger = require('@alias/logger')('growi:migrate:abolish-myrelations');
const mongoose = require('mongoose');
const config = require('@root/config/migrate');
/**
* BEFORE
* - 'myrelations' collection exists (related to models/myrelations.js)
* - schema:
* {
* "_id" : ObjectId("5bc9de4d745e137e0424ed89"),
* "group" : ObjectId("5b028f13c1f7ba2e58d2fd21"),
* "user" : ObjectId("5b07e6e6929bad5d3cce9995"),
* "__v" : 0
* }
* AFTER
* - 'myrelations' collection is dropped and models/myrelations.js is removed
*/
module.exports = {
async up(db) {
logger.info('Apply migration');
mongoose.connect(config.mongoUri, config.mongodb.options);
const User = require('@server/models/user')();
const UserGroup = require('@server/models/user-group')();
// retrieve all documents from 'myrelations'
const relations = await db.collection('myrelations').find().toArray();
for (let relation of relations) {
const user = await User.findOne({ _id: relation.user });
const group = await UserGroup.findOne({ _id: relation.group });
...
await user.save();
await group.save();
}
// drop collection
await db.collection('myrelations').drop();
logger.info('Migration has successfully applied');
},
async down(db) {
logger.info('Undo migration');
mongoose.connect(config.mongoUri, config.mongodb.options);
const User = require('@server/models/user')();
const UserGroup = require('@server/models/user-group')();
const insertDocs = ...;
...
await db.collection('myrelations').insertMany(insertDocs);
logger.info('Migration has successfully undoed');
}
};
コード解説
上記コードでは、 myrelations
という関連テーブル風のコレクションで管理していた情報を破棄する、というシナリオを想定しました。myrelations
コレクションは当初は Mongoose によって管理されていたモデルに対応する形で生成されたはずですが、モデルを廃止する場合は当然モデルクラス・モジュールも削除されますので、スキーマファイルを require してアクセスすることができません。そのため、Raw な MongoDB Driver API を用いてデータマイグレーションを行う必要があります。
まとめ
まだまだ発展途上の JavaScript、Node.js の世界では、MongoDB を利用しながらデータマイグレーション機構をプロジェクトに導入できるデファクトスタンダードがまだありません。しかしながらコミュニティは精力的に npm ライブラリを開発・追加し続けていますので、今回のように慎重に選定を行い、時にはいくつかの実装を実際に試しながら導入することで、他の枯れた言語・エコシステムに劣らないプロジェクト構成を実現できます。
今回の Tips が、JavaScript と MongoDB を好むプロジェクトたちの一助となれば幸いです。