2019/08/19

Sequelize-autoでリレーションも付ける(NodeJSでER図先行開発)

NodeJSのRDB向けのORMである、Sequelizeを使ってER図先行開発をする方法を紹介します。Sequelizeの使い方ではなくSequelizeで使うためのモデルクラスを、既存のDBのテーブルから自動生成する方法です。
これができると、

  1. ツールを使ってER図を描く
  2. SQL(AlterSQL)を発行してテーブルを作る
  3. モデルクラスを自動生成してコーディングする

という開発手順を回せます。この手順を確立するとプロジェクトファイルにER図が常に正しい状態で存在することになり、開発の継続性能がぜんぜん違います。
もし説明を長々と読むのが嫌な場合、erf_devのmodels/index.jsを見てください。また、これのためにモデルを生成するコマンドはpackage.jsonのdb:modelsを見てください。
既存のテーブルからモデルを自動生成するためにSequelize-autoを使います。Sequelize-autoはモデルクラスを自動生成するツールなので、贅沢を言わなければ「MySQL Workbench」などのER図ツールからSQLを発行してテーブルを構築し、Sequelize-autoでモデルクラスを生成すれば終わりです。ですがこれだけだと外部キーによる子テーブルのデータ取得ができず、ORMとしては致命的です。

Sequelizeでリレーションを扱うには、モデルクラスを定義した後でそれぞれのモデルに対し、hasMany()やbelongsTo()などのリレーションを設定します。
Sequelize-autoではそこまでのコードは生成しないようなので、ややトリッキーなことをします。

① 1対1の関係、1対多の関係を設定する

元々のアイデアはsequelize-auto/issues/34 にあるもので、例えばuserテーブルとuser_bookテーブルが「1対多」の関係の場合、user_tableにはuserのidとして「user_id」があるはずです。

実際には「user_id」という名前でないカラム名を付けることもできますが、慣例として(Javaであれば大文字始まりはクラス名というくらい常識的な命名規則として)「テーブル名_id」というカラム名で外部キーを設定するはずです。
この命名を利用して「Id」でsplitして連携先のテーブルモデルを探し出し、そのモデルと連携させます。文字列処理をするため、少し違う命名でテーブルを作ってしまうと正常に動かなくなってしまう、という危険はありますが、逆にこのことさえ理解してテーブル設計をすれば非常に便利に連携まで作れるようになります。

上の例ではuser.hasMany(user_book)と、user_book.belongsTo(user)の2つを設定します。
上記Issuesには完全なリレーション設定までのサンプルコードは載っていないですが、少し工夫すれば作れます(erf_devのmodels/index.jsを見てください)
もし1対1ならhasMany, belongsToの代わりにお互いにhasOne( )を設定します。

② 多対多の関係を設定する

多対多の場合はさらにトリッキーとなり、元々RDBでは多対多のテーブルは扱えないため、間にJunctionテーブルを設けます。よくある例で、学生は複数の講義を受講し、講義は複数の学生が受講する、と言う関係では、学生テーブル、受講テーブル、講義テーブル、と言う構成にします。

Sequelizeでトリッキーなことをしないでも基礎的なhasMany( )belongsTo( )だけで、学生から複数の受講を取得し、受講から各講義を取り出す、という2段構えで取得できます(講義も同様で講義から複数の受講を取得し、受講から各学生の情報を取り出すという方法です)。

ただSequelizeには多対多の関係を扱える仕組みが使えます。そのためには間のJunctionテーブルを「テーブル名_has_テーブル名」という命名規則にして、「_has_」でsplitし、互いのモデルクラスを直接多対多として結びつける、ということを行います。
学生.belongsToMany(講義)と、講義.belongsToMany(学生)として、間のJunctionテーブルを隠して直接学生から受講している講義を、講義からは受講生を取得できるようになります。

③ その他の設定

最後にテーブルのカラム名はスネークケースですが、JavaScriptの要素としてはキャメルケースで取る、という慣例に沿ってSequelize-auto、Sequelizeの設定をして完成です。

①、②のコードの主要な部分は以下です。
/**
 *  setup
 *  belongsTo(), belongsToMany(), hasMany(), hasOne()
 *  based on references
 */
log('────────────────────────────────────────────────');
sequelize.modelManager.models.forEach(model => {

  const isJunctionTable = model.tableName.split('_has_').length === 2;
  if (isJunctionTable) {
    const tables = Object.getOwnPropertyNames(model.rawAttributes)
      .filter(attributeName => undefined !== model.rawAttributes[attributeName].references)
      .map(attributeName => {
          const refModel = sequelize.modelManager.models
            .find(testModel => testModel.tableName === model.rawAttributes[attributeName].references.model);
          return {
            model: refModel,
            through: {
              through: model,
              as: attributeName.split('Id')[0],
            }
          };
        }
      );

    log(tables[0].model.tableName, ' 1..* ──────── 1..* ', tables[1].model.tableName);

    tables[1].model.belongsToMany(tables[0].model, tables[0].through);
    tables[0].model.belongsToMany(tables[1].model, tables[1].through);

  } else {
    Object.getOwnPropertyNames(model.rawAttributes).forEach(attributeName => {
      if (model.rawAttributes[attributeName].references) {

        const refModel = sequelize.modelManager.models
          .find(testModel => testModel.tableName === model.rawAttributes[attributeName].references.model);
        const belongsToOptions = {
          foreignKey: model.rawAttributes[attributeName].field.toString(),
          as: attributeName.split('Id')[0],
        };

        const options = {};
        if (model.rawAttributes[attributeName].primaryKey) {
          log(refModel.tableName, ' 1 ──────── 1 ', model.tableName);
          refModel.hasOne(model, options);
        } else {
          log(refModel.tableName, ' 1 ──────── 1..* ', model.tableName);
          refModel.hasMany(model, options);
        }
        model.belongsTo(refModel, belongsToOptions);
      }
    });
  }
});
log('────────────────────────────────────────────────');
また、erf_devのmodels/index.jsも見てください。
①、②の命名規則はerf_devのER_Diagram.mdを読んでください。
③については、
erf_devのpackage.jsonと、erf_devのsequelize-auto_additional.jsonを見てください。

バラバラでは分かりにくいはずなので、全てをまとめて動かしてみるには、
是非erf_devのTutorial.mdを試してみください。


How to Automatic generate "sequelize models" with All associations from MySQL Tables.

0 件のコメント:

コメントを投稿