sequelize でテーブル結合:hasOne と belongsTo の違いやオプションの指定など

sequelizeロゴ 開発
スポンサーリンク

sequelize で結合する方法です。

sequelize でも 一対一(One-To-One)、一対多(One-To-Many)、多対多(Many-To-Many)といった結合が可能です。この結合関係を作るために4つの結合方法(HasOne、BelongsTo、HasMany、BelongsToMany)が提供されてます。

結合の定義方法

結合の定義は簡単です。どの種類にせよ、関数の呼び出しだけで定義することができます。以下では定義済みのモデルAとモデルBを仮定します。

const A = sequelize.define('A', /* ... */);
const B = sequelize.define('B', /* ... */);

A.hasOne(B); // A と B が One-To-One の関係。FKはモデルBで定義される
A.belongsTo(B); // A と B が One-To-One の関係。FKはモデルAで定義される
A.hasMany(B); // A と B が One-To-Many の関係。FKはモデルBで定義される
A.belongsToMany(B, {
  through: 'C'
});
// A と B が中間テーブル C を介して Many-To-Many の関係
// C は定義済みのモデルでも良いし、存在しなければ Sequelize が生成する

どの関数でも第二引数に option オブジェクトを渡すことができます。hasOne、belongsTo、hasMany に関しては任意、belongsToMany の場合は少なくとも through プロパティだけは必須です。

hasOne と belongsTo の違い

どちらも One-To-One の関係を表現します。違いは何でしょうか?

意味的な違いとしては、A.hasOne(B) や B.belongsTo(A) のとき A は B が存在しなくても存在できますが、B は A がなれけば存在できません。下記リンクの例では、Man.hasOne(RightArm) と RightArm.belongsTo(Man) のように人間モデルと右腕モデルで説明しています。

そして、外部キーの作成場所が異なります。B.belongsTo(A) と A.hasOne(B) はどちらも B 側に外部キーのカラムを追加します。

参考: https://stackoverflow.com/questions/34565360/difference-between-hasone-and-belongsto-in-sequelize-orm

hasOne と belongsTo はどちらも定義する必要がある?

hasOne と belongsTo 、hasMany と belongsTo 、belongsToMany と belongsToMany の結合はペアで使われるよーって公式に書いてます。

ひとつの宣言では、ソース側のモデルしかそのことを認識できないためです。

例えば A.hasOne(B) としたとき、A がソースモデルで B がターゲットモデルです。この宣言では A は結合の存在をわかっていますが、B は関係について何も知りません。つまり、A.findAll({ include: B}) は実行できますが、 B.findAll({ include: A }) は実行できません

ということで、必要なければ片方だけでも十分ですが、Sequelize の機能を十分に使うには、結合関係はペアで宣言しておくのが良いです。

詳しい解説は ここ です。

外部キーの設定

モデルFooとモデルBarの一対一関係は次のように定義できます。

Foo.hasOne(Bar);
Bar.belongsTo(Foo);

前述のように、この定義で Sequelize はモデルBarに fooId カラムがあることを想定します。そして次のような SQL が実行されます。

CREATE TABLE IF NOT EXISTS "foos" (
  /* ... */
);
CREATE TABLE IF NOT EXISTS "bars" (
  /* ... */
  "fooId" INTEGER REFERENCES "foos" ("id") ON DELETE SET NULL ON UPDATE CASCADE
  /* ... */
);

上の例ではオプションを指定していないので Sequelize が自動で外部キー名を設定してます。違う名前を使いたい場合にはオプションで指定することができます。

Foo.hasOne(Bar, {
  foreignKey: 'myFooId'
});
Bar.belongsTo(Foo);

Foo.hasOne(Bar, {
  foreignKey: {
    name: 'myFooId'
  }
});
Bar.belongsTo(Foo);

外部キーは文字列で渡しても良いですし、オブジェクトを渡すこともできます。オブジェクトを渡す場合は、 sequelize.define のときと同じように扱われるので type や allowNull などといったオプションを指定することもできます。例えば、外部キーのデフォルトは INTEGER が使われますが、別の型を指定することもできます。

Eager Loading vs Lazy Loading

Lazy Loading

Lazy Loading は、必要なときにだけ読み込む方法です。

const awesomeCaptain = await Captain.findOne({
  where: {
    name: "Jack Sparrow"
  }
});
// モデル Captain の取得結果を処理
console.log('Name:', awesomeCaptain.name);
console.log('Skill Level:', awesomeCaptain.skillLevel);

// 結合されたモデル Ship の情報を取得
const hisShip = await awesomeCaptain.getShip();

// モデル Ship の取得結果を処理
console.log('Ship Name:', hisShip.name);
console.log('Amount of Sails:', hisShip.amountOfSails);

この方法では、必要な時にだけロードするので時間とメモリを節約することができます。getShip() は Sequelize が自動的に Captain インスタンスに追加したメソッドのひとつです。

Eager Loading

const awesomeCaptain = await Captain.findOne({
  where: {
    name: "Jack Sparrow"
  },
  include: Ship
});

// モデル Ship の情報は既に取得されている
console.log('Name:', awesomeCaptain.name);
console.log('Skill Level:', awesomeCaptain.skillLevel);
console.log('Ship Name:', awesomeCaptain.ship.name);
console.log('Amount of Sails:', awesomeCaptain.ship.amountOfSails);

include オプションでモデル Ship を指定しています。クエリ実行は一度で済みます。

複数のテーブルを結合したい場合

const awesomeCaptain = await Captain.findOne({
  where: {
	  name: "Jack Sparrow"
	},
	include: [{
	  model: Ship,
	}, {
	  model: Ocean
	}],
});

const awesomeCaptain = await Captain.findOne({
  where: {
	  name: "Jack Sparrow",
	},
	include: [
  	// model: Model だけなら 省略することもできる
	  Ship,
		{
		  model: Ocean,
			attribute: [/** attribute **/],
		}
	],
});

複数のテーブルを結合したいときは include に配列を渡せば良い。他のオプションが必要なければモデルだけを渡して表記を省略することもできます。

タイトルとURLをコピーしました