Katashin .info

Bookshelf のアソシエーションで発生する循環読み込みによるエラーを回避する

Node.js の ORM である Bookshelf では公式サイトの例のようにアソシエーションを定義することができます。 しかし、モデルの定義を別ファイルに分けるとこの例が動かなくなります。 この記事では、モデルの定義を別ファイルに分けても意図通りに動くコードの書き方の説明と、それを支援する Bookshelf プラグインの紹介をします。

循環読み込みする例 #

以下のコードは page.js 内で book.js を読み込み、 book.js 内で page.js を読み込んでおり、循環読み込みが発生しています。 例えばこの状態で、別のファイルから page.js を読み込むと、 book.js 内の require('./page'){} を返し、 Book モデルの pages アソシエーションを使用するときにエラーが発生します。

// book.js
const Page = require('./page')

const Book = bookshelf.Model.extend({
  pages: function () {
    return this.hasMany(Page)
  },
})

module.exports = Book
// page.js
const Book = require('./book')

const Page = bookshelf.Model.extend({
  book: function () {
    return this.belongsTo(Book)
  },
})

module.exports = Page

解決策 #

この問題については、生成したモデルを保持するオブジェクトを別で定義するというのが一般的な解決策だと思います。 つまり、以下のようなコードになります。 以下のコードは、 require の循環読み込みが発生しているものの、実際にモデルが利用されるときには models 以下に定義したモデルが格納されているため、意図した通りの動作になります。

// book.js
const models = require('./models')

require('./page')
const Book = bookshelf.Model.extend({
  pages: function () {
    return this.hasMany(models.Page)
  },
})

module.exports = models.Book = Book
// page.js
const models = require('./models')

require('./book')
const Page = bookshelf.Model.extend({
  book: function () {
    return this.belongsTo(models.Book)
  },
})

module.exports = models.Page = Page
// models.js
exports = {}

あらかじめ全て読み込んでおく例 #

上記の解決策でも動くのですが、各モデルのファイルで依存するファイルを読み込む必要があります(require してる部分)。なぜなら、あるモデルを利用する時に、依存する他のモデルの生成が完了しているとは限らないためです。 モデルの数が増えてくると、依存関係を書くのがめんどくさくなってくると思います。また、漏れがあった時に気づくことが困難です。

よって、 models.js 内であらかじめモデルをすべて読み込んでおくのも良いのではないかと思います。 各モデルのファイルから依存するモデルを読み込む require を削除し、 models.js を以下のようにします。 MODEL_DIR には、各モデルのファイルが入っているディレクトリを指定します。各モデルのファイルと models.js の間で循環読み込みしていますが、 exports = {}; の後に require しているため、 require('./models') は意図したオブジェクトを返してくれます。

// models.js
exports = {};

const fs = require('fs');
const MODEL_DIR = './';
fs.readdirSync(MODEL_DIR)
  .filter((filename) => 'models.js' !== filename)
  .forEach((filename) => {
    require(MODEL\_DIR + filename);
  });

ES6 Proxy を使って必要なときに読み込む例 #

すべて読み込むのではなく、必要となったタイミングで読み込むようなコードは以下のようになります。 Proxy を使わなくても models('ModelName') でモデルを返すような関数を定義すれば同じようなことは実現できます(文字列で書きたくなかった)。Node.js ではまだデフォルトで Proxy を使えない点と、最新の記法を使うために Polyfill が必要な点、また、モデル名をファイル名に変換する処理が必要な点に気をつける必要があります。(執筆時の Node.js の最新版は 5.4.0)

const _ = require('lodash');
const MODEL_DIR = './';

const proxy = new Proxy({}, {
  get: function(models, name) {
    return models[name] || (models[name] = require(MODEL_DIR + _.snakeCase(name));
  }
});

exports = proxy;

registry プラグイン #

上記のようなことをやってくれる Bookshelf のプラグインが標準で提供されています。

Plugin: Model Registry · tgriesser/bookshelf Wiki

bookshelf.model('ModelName', Model) でモデルを登録し、 this.hasMany('ModelName') のように、アソシエーションの時に文字列でモデル名を指定することができます。 しかし、アソシエーション以外で、モデルを利用する時に、 bookshelf.model('ModelName') と書くのがめんどくさいです。個人的には自前で書いてしまう方が良いように思います。

まとめ #

Bookshelf のモデルの定義を別ファイルに分けると、単純な書き方では循環読み込みでエラーが発生するようになります。 この問題は、生成したモデルを保持するオブジェクトを別で定義することで解決することができます。 モデルを定義する作業を楽にするために、モデルのファイルをあらかじめすべて読み込む処理を書いたり、モデルが必要となったタイミングで動的に読み込む処理を書くのも有効です。 また、Bookshelf 標準でこういった問題を解決してくれる registry というプラグインが提供されています。