JavaScript/Node.jsでMongoDB+Mongooseのデータマイグレーション

古くは「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 を好むプロジェクトたちの一助となれば幸いです。