メインコンテンツにスキップ
バージョン: v6 - 安定版

関連付け

Sequelize は、標準的な関連付けをサポートしています: 1対1, 1対多, そして 多対多

これを実現するために、Sequelize は、それらを作成するために組み合わせるべき **4 つ** の関連付けタイプを提供します。

  • HasOne 関連付け
  • BelongsTo 関連付け
  • HasMany 関連付け
  • BelongsToMany 関連付け

このガイドでは、まずこれら 4 つの関連付けタイプを定義する方法を説明し、次にそれらを組み合わせて 3 つの標準的な関連付けタイプ (1対1, 1対多, および 多対多) を定義する方法を説明します。

Sequelize 関連付けの定義

4 つの関連付けタイプは、非常に似た方法で定義されます。2 つのモデル AB があるとしましょう。Sequelize に、この 2 つのモデル間に関連付けを設定したいと伝えるには、関数呼び出しが必要です。

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

A.hasOne(B); // A HasOne B
A.belongsTo(B); // A BelongsTo B
A.hasMany(B); // A HasMany B
A.belongsToMany(B, { through: 'C' }); // A BelongsToMany B through the junction table C

これらはすべて、2 番目のパラメーターとしてオプションオブジェクトを受け取ります (最初の 3 つはオプション、belongsToMany は少なくとも through プロパティを含む必須です)。

A.hasOne(B, {
/* options */
});
A.belongsTo(B, {
/* options */
});
A.hasMany(B, {
/* options */
});
A.belongsToMany(B, { through: 'C' /* options */ });

関連付けが定義される順序は重要です。つまり、4 つのケースすべてにおいて、順序が重要です。上記のすべての例では、A が **ソース** モデルと呼ばれ、B が **ターゲット** モデルと呼ばれます。この用語は重要です。

A.hasOne(B) 関連付けは、AB の間に 1 対 1 の関係が存在し、外部キーがターゲットモデル (B) で定義されることを意味します。

A.belongsTo(B) 関連付けは、AB の間に 1 対 1 の関係が存在し、外部キーがソースモデル (A) で定義されることを意味します。

A.hasMany(B) 関連付けは、AB の間に 1 対多の関係が存在し、外部キーがターゲットモデル (B) で定義されることを意味します。

これらの 3 つの呼び出しにより、Sequelize は、(既に存在する場合を除き) 適切なモデルに自動的に外部キーを追加します。

A.belongsToMany(B, { through: 'C' }) 関連付けは、テーブル C結合テーブル として使用して、AB の間に多対多の関係が存在することを意味します。結合テーブルには外部キー (たとえば、aIdbId) が設定されます。Sequelize は、このモデル C を自動的に作成し (既に存在する場合を除く)、適切な外部キーを定義します。

注: 上記の belongsToMany の例では、文字列 ('C') が through オプションに渡されました。この場合、Sequelize はこの名前でモデルを自動的に生成します。ただし、既に定義している場合は、モデルを直接渡すこともできます。

これらは、各関連付けタイプに関与する主なアイデアです。ただし、これらの関係は、Sequelize でより適切に使用できるようにするために、ペアで使用されることがよくあります。これについては後で説明します。

標準的な関係の作成

前述したように、通常、Sequelize の関連付けはペアで定義されます。要約すると

  • **1対1** の関係を作成するには、hasOnebelongsTo の関連付けを一緒に使用します。
  • **1対多** の関係を作成するには、hasManybelongsTo の関連付けを一緒に使用します。
  • **多対多** の関係を作成するには、2 つの belongsToMany 呼び出しを一緒に使用します。

これについてはすべて、次に詳しく説明します。単一の関連付けではなく、これらのペアを使用する利点については、この章の最後に説明します。

1対1の関係

哲学

Sequelize の使用について詳しく掘り下げる前に、1対1の関係で何が起こるかを振り返ってみることは有益です。

2 つのモデル FooBar があるとしましょう。Foo と Bar の間に 1 対 1 の関係を確立したいとします。リレーショナルデータベースでは、これはいずれかのテーブルに外部キーを確立することで行われることを知っています。したがって、この場合、非常に重要な質問は、この外部キーをどのテーブルに配置するかということです。つまり、FoobarId 列を持たせたいか、それとも Bar に代わりに fooId 列を持たせるべきでしょうか。

原則として、どちらのオプションも Foo と Bar の間に 1 対 1 の関係を確立する有効な方法です。ただし、「Foo と Bar の間には 1 対 1 の関係がある」と言う場合、その関係が 必須 であるかオプションであるかは不明確です。つまり、Bar がなくても Foo は存在できますか?Foo がなくても Bar は存在できますか?これらの質問への答えは、外部キー列をどこに配置するかを理解するのに役立ちます。

目標

この例の残りの部分では、FooBar の 2 つのモデルがあると仮定します。BarfooId 列を取得するように、それらの間に 1 対 1 の関係を設定したいと考えています。

実装

目標を達成するための主な設定は次のとおりです。

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

オプションが渡されていないため、Sequelize はモデルの名前から何をすべきかを推論します。この場合、Sequelize は fooId 列を Bar に追加する必要があることを認識します。

この方法で、上記の後に Bar.sync() を呼び出すと、次の SQL が生成されます (たとえば PostgreSQL の場合)。

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

オプション

さまざまなオプションを、関連付け呼び出しの 2 番目のパラメーターとして渡すことができます。

onDeleteonUpdate

たとえば、ON DELETEON UPDATE の動作を設定するには、次のようにします。

Foo.hasOne(Bar, {
onDelete: 'RESTRICT',
onUpdate: 'RESTRICT',
});
Bar.belongsTo(Foo);

選択できるのは、RESTRICTCASCADENO ACTIONSET DEFAULT、および SET NULL です。

1対1の関連付けのデフォルトは、ON DELETE では SET NULLON UPDATE では CASCADE です。

外部キーのカスタマイズ

上記に示した hasOnebelongsTo の両方の呼び出しは、作成される外部キーを fooId とする必要があることを推測します。myFooId など、別の名前を使用するには

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

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

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

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

上記に示すように、foreignKey オプションは、文字列またはオブジェクトを受け取ります。オブジェクトを受け取ると、このオブジェクトは、標準の sequelize.define 呼び出しで実行する場合と同様に、列の定義として使用されます。したがって、typeallowNulldefaultValue などのオプションを指定すると、機能します。

たとえば、デフォルト (INTEGER) の代わりに、外部キーのデータ型として UUID を使用するには、次のようにします。

const { DataTypes } = require('Sequelize');

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

必須とオプションの関連付け

デフォルトでは、関連付けはオプションとみなされます。言い換えれば、この例では、fooIdはnullになることが許容されており、つまり、1つのBarがFooなしで存在できます。これを変更するには、外部キーオプションでallowNull: falseを指定するだけです。

Foo.hasOne(Bar, {
foreignKey: {
allowNull: false,
},
});
// "fooId" INTEGER NOT NULL REFERENCES "foos" ("id") ON DELETE RESTRICT ON UPDATE RESTRICT

一対多の関係

考え方

一対多の関連付けは、1つのソースを複数のターゲットに接続しますが、これらのターゲットはすべて、この単一のソースとのみ接続されます。

つまり、外部キーをどこに配置するかを選択する必要があった一対一の関連付けとは異なり、一対多の関連付けには1つのオプションしかありません。たとえば、1つのFooが多くのBarを持っている場合(そして、この方法で各Barが1つのFooに属している場合)、唯一理にかなった実装は、BarテーブルにfooId列を持つことです。1つのFooが多くのBarを持つため、逆は不可能です。

目標

この例では、TeamPlayerというモデルがあります。1つのチームには多くのプレイヤーがおり、各プレイヤーは単一のチームに属するという意味で、それらの間に一対多の関係があることをSequelizeに伝えたいと考えています。

実装

これを行う主な方法は次のとおりです

Team.hasMany(Player);
Player.belongsTo(Team);

繰り返しますが、前述のように、これを行う主な方法は、Sequelizeの関連付け(hasManybelongsTo)のペアを使用することです。

たとえば、PostgreSQLでは、上記のセットアップによりsync()時に次のSQLが生成されます。

CREATE TABLE IF NOT EXISTS "Teams" (
/* ... */
);
CREATE TABLE IF NOT EXISTS "Players" (
/* ... */
"TeamId" INTEGER REFERENCES "Teams" ("id") ON DELETE SET NULL ON UPDATE CASCADE,
/* ... */
);

オプション

この場合に適用されるオプションは、一対一の場合と同じです。たとえば、外部キーの名前を変更し、関係が必須であることを確認するには、次のようにします。

Team.hasMany(Player, {
foreignKey: 'clubId',
});
Player.belongsTo(Team);

一対一の関係と同様に、ON DELETEはデフォルトでSET NULLになり、ON UPDATEはデフォルトでCASCADEになります。

多対多の関係

考え方

多対多の関連付けは、1つのソースを複数のターゲットに接続しますが、これらのターゲットは順番に、最初のソース以外の他のソースにも接続できます。

これは、他の関係のように、いずれかのテーブルに1つの外部キーを追加することでは表現できません。代わりに、結合モデルの概念が使用されます。これは、2つの外部キー列を持ち、関連付けを追跡する追加のモデル(およびデータベース内の追加のテーブル)になります。結合テーブルは、join tableまたはthrough tableとも呼ばれます。

目標

この例では、MovieActorというモデルを考えます。1人の俳優は多くの映画に出演している可能性があり、1つの映画には制作に関わる多くの俳優がいます。関連付けを追跡する結合テーブルはActorMoviesと呼ばれ、外部キーmovieIdactorIdが含まれます。

実装

Sequelizeでこれを行う主な方法は次のとおりです。

const Movie = sequelize.define('Movie', { name: DataTypes.STRING });
const Actor = sequelize.define('Actor', { name: DataTypes.STRING });
Movie.belongsToMany(Actor, { through: 'ActorMovies' });
Actor.belongsToMany(Movie, { through: 'ActorMovies' });

belongsToMany呼び出しのthroughオプションに文字列が指定されているため、Sequelizeは結合モデルとして機能するActorMoviesモデルを自動的に作成します。たとえば、PostgreSQLでは

CREATE TABLE IF NOT EXISTS "ActorMovies" (
"createdAt" TIMESTAMP WITH TIME ZONE NOT NULL,
"updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL,
"MovieId" INTEGER REFERENCES "Movies" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
"ActorId" INTEGER REFERENCES "Actors" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
PRIMARY KEY ("MovieId","ActorId")
);

文字列の代わりに、モデルを直接渡すこともサポートされており、その場合、指定されたモデルが結合モデルとして使用されます(モデルは自動的に作成されません)。例えば

const Movie = sequelize.define('Movie', { name: DataTypes.STRING });
const Actor = sequelize.define('Actor', { name: DataTypes.STRING });
const ActorMovies = sequelize.define('ActorMovies', {
MovieId: {
type: DataTypes.INTEGER,
references: {
model: Movie, // 'Movies' would also work
key: 'id',
},
},
ActorId: {
type: DataTypes.INTEGER,
references: {
model: Actor, // 'Actors' would also work
key: 'id',
},
},
});
Movie.belongsToMany(Actor, { through: ActorMovies });
Actor.belongsToMany(Movie, { through: ActorMovies });

上記はPostgreSQLで次のSQLを生成します。これは上に示したものと同等です

CREATE TABLE IF NOT EXISTS "ActorMovies" (
"MovieId" INTEGER NOT NULL REFERENCES "Movies" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
"ActorId" INTEGER NOT NULL REFERENCES "Actors" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
"createdAt" TIMESTAMP WITH TIME ZONE NOT NULL,
"updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL,
UNIQUE ("MovieId", "ActorId"), -- Note: Sequelize generated this UNIQUE constraint but
PRIMARY KEY ("MovieId","ActorId") -- it is irrelevant since it's also a PRIMARY KEY
);

オプション

一対一および一対多の関係とは異なり、多対多の関係では、ON UPDATEON DELETEの両方のデフォルトはCASCADEです。

Belongs-To-Manyは、throughモデルに一意のキーを作成します。この一意のキー名は、uniqueKeyオプションを使用してオーバーライドできます。この一意のキーの作成を防ぐには、unique: falseオプションを使用します。

Project.belongsToMany(User, {
through: UserProjects,
uniqueKey: 'my_custom_unique',
});

関連付けを含むクエリの基本

関連付けを定義する基本を説明したので、関連付けを含むクエリを見てみましょう。この件に関する最も一般的なクエリは、読み取りクエリ(つまりSELECT)です。後で、他のタイプのクエリについて説明します。

これを検討するために、船と船長があり、それらの間に一対一の関係がある例を考えます。外部キーにnullを許可します(デフォルト)。つまり、船は船長なしで存在でき、その逆も同様です。

// This is the setup of our models for the examples below
const Ship = sequelize.define(
'ship',
{
name: DataTypes.TEXT,
crewCapacity: DataTypes.INTEGER,
amountOfSails: DataTypes.INTEGER,
},
{ timestamps: false },
);
const Captain = sequelize.define(
'captain',
{
name: DataTypes.TEXT,
skillLevel: {
type: DataTypes.INTEGER,
validate: { min: 1, max: 10 },
},
},
{ timestamps: false },
);
Captain.hasOne(Ship);
Ship.belongsTo(Captain);

関連付けのフェッチ - 遅延ロードと先行ロード

遅延ロードと先行ロードの概念は、Sequelizeで関連付けをフェッチする方法を理解する上で基本です。遅延ロードとは、関連付けられたデータを実際に必要なときにのみフェッチする手法を指します。一方、先行ロードとは、最初からすべてを一度に、より大きなクエリでフェッチする手法を指します。

遅延ロードの例

const awesomeCaptain = await Captain.findOne({
where: {
name: 'Jack Sparrow',
},
});
// Do stuff with the fetched captain
console.log('Name:', awesomeCaptain.name);
console.log('Skill Level:', awesomeCaptain.skillLevel);
// Now we want information about his ship!
const hisShip = await awesomeCaptain.getShip();
// Do stuff with the ship
console.log('Ship Name:', hisShip.name);
console.log('Amount of Sails:', hisShip.amountOfSails);

上記の例では、関連付けられた船を使用したい場合にのみフェッチし、2つのクエリを実行したことに注目してください。これは、船が必要になるかもしれないし、必要ないかもしれない場合、おそらくいくつかのケースでのみ条件付きでフェッチしたい場合に特に役立ちます。このようにして、必要な場合にのみフェッチすることで時間とメモリを節約できます。

注:上記で使用されているgetShip()インスタンスメソッドは、SequelizeがCaptainインスタンスに自動的に追加するメソッドの1つです。他にもあります。このガイドの後半でそれらについて詳しく学びます。

先行ロードの例

const awesomeCaptain = await Captain.findOne({
where: {
name: 'Jack Sparrow',
},
include: Ship,
});
// Now the ship comes with it
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);

上記のように、Sequelizeでの先行ロードは、includeオプションを使用することで実行されます。ここでは、関連付けられたデータとインスタンスを一緒に取得するために、データベースに対して1つのクエリのみが実行されたことに注目してください。

これは、Sequelizeでの先行ロードの簡単な紹介にすぎませんでした。詳細については、先行ロードに関する専用ガイドで学ぶことができます。

作成、更新、削除

上記では、関連付けを含むデータのフェッチに関するクエリの基本を示しました。作成、更新、削除については、次のいずれかを行うことができます。

  • 標準モデルクエリを直接使用する

    // Example: creating an associated model using the standard methods
    Bar.create({
    name: 'My Bar',
    fooId: 5,
    });
    // This creates a Bar belonging to the Foo of ID 5 (since fooId is
    // a regular column, after all). Nothing very clever going on here.
  • または、関連付けられたモデルで使用できる特別なメソッド/ミックスインを使用します。これについては、このページで後ほど説明します。

注: save()インスタンスメソッドは、関連付けを認識しません。つまり、オブジェクトと一緒に先行ロードされたオブジェクトの値を変更した場合、親でsave()を呼び出すと、子で発生した変更が完全に無視されます。

関連付けエイリアスとカスタム外部キー

上記のすべての例では、Sequelizeは外部キー名を自動的に定義しました。たとえば、船と船長の例では、Sequelizeは自動的に船モデルにcaptainIdフィールドを定義しました。ただし、カスタム外部キーを指定するのは簡単です。

現在のトピックに焦点を当てるために、以下に示すように(フィールドを減らして)簡略化した形式で、船と船長のモデルを考えてみましょう。

const Ship = sequelize.define('ship', { name: DataTypes.TEXT }, { timestamps: false });
const Captain = sequelize.define('captain', { name: DataTypes.TEXT }, { timestamps: false });

外部キーに異なる名前を指定するには、3つの方法があります。

  • 外部キー名を直接指定する
  • エイリアスを定義する
  • 両方を行う

まとめ:デフォルトのセットアップ

単にShip.belongsTo(Captain)を使用することで、sequelizeは外部キー名を自動的に生成します

Ship.belongsTo(Captain); // This creates the `captainId` foreign key in Ship.

// Eager Loading is done by passing the model to `include`:
console.log((await Ship.findAll({ include: Captain })).toJSON());
// Or by providing the associated model name:
console.log((await Ship.findAll({ include: 'captain' })).toJSON());

// Also, instances obtain a `getCaptain()` method for Lazy Loading:
const ship = Ship.findOne();
console.log((await ship.getCaptain()).toJSON());

外部キー名を直接指定する

外部キー名は、関連付け定義のオプションを使用して直接指定できます。例:

Ship.belongsTo(Captain, { foreignKey: 'bossId' }); // This creates the `bossId` foreign key in Ship.

// Eager Loading is done by passing the model to `include`:
console.log((await Ship.findAll({ include: Captain })).toJSON());
// Or by providing the associated model name:
console.log((await Ship.findAll({ include: 'Captain' })).toJSON());

// Also, instances obtain a `getCaptain()` method for Lazy Loading:
const ship = await Ship.findOne();
console.log((await ship.getCaptain()).toJSON());

エイリアスを定義する

エイリアスの定義は、外部キーにカスタム名を指定するよりも強力です。これは、例でよりよく理解できます。

Ship.belongsTo(Captain, { as: 'leader' }); // This creates the `leaderId` foreign key in Ship.

// Eager Loading no longer works by passing the model to `include`:
console.log((await Ship.findAll({ include: Captain })).toJSON()); // Throws an error
// Instead, you have to pass the alias:
console.log((await Ship.findAll({ include: 'leader' })).toJSON());
// Or you can pass an object specifying the model and alias:
console.log(
(
await Ship.findAll({
include: {
model: Captain,
as: 'leader',
},
})
).toJSON(),
);

// Also, instances obtain a `getLeader()` method for Lazy Loading:
const ship = await Ship.findOne();
console.log((await ship.getLeader()).toJSON());

エイリアスは、同じモデル間で2つの異なる関連付けを定義する必要がある場合に特に役立ちます。たとえば、MailPersonというモデルがある場合、メールの送信者受信者を表すために2回関連付けたい場合があります。この場合、mail.getPerson()のような呼び出しがあいまいになるため、各関連付けにエイリアスを使用する必要があります。senderreceiverのエイリアスを使用すると、mail.getSender()mail.getReceiver()の両方のメソッドが利用可能になり、機能します。どちらもPromise<Person>を返します。

hasOneまたはbelongsTo関連付けのエイリアスを定義する場合は、(上記の例のleaderなど)単数形の単語を使用する必要があります。一方、hasManybelongsToManyのエイリアスを定義する場合は、複数形を使用する必要があります。多対多の関係のエイリアス(belongsToManyを使用)の定義については、高度な多対多の関連付けガイドで説明します。

両方を行う

エイリアスを定義し、外部キーを直接定義することもできます

Ship.belongsTo(Captain, { as: 'leader', foreignKey: 'bossId' }); // This creates the `bossId` foreign key in Ship.

// Since an alias was defined, eager Loading doesn't work by simply passing the model to `include`:
console.log((await Ship.findAll({ include: Captain })).toJSON()); // Throws an error
// Instead, you have to pass the alias:
console.log((await Ship.findAll({ include: 'leader' })).toJSON());
// Or you can pass an object specifying the model and alias:
console.log(
(
await Ship.findAll({
include: {
model: Captain,
as: 'leader',
},
})
).toJSON(),
);

// Also, instances obtain a `getLeader()` method for Lazy Loading:
const ship = await Ship.findOne();
console.log((await ship.getLeader()).toJSON());

インスタンスに追加される特別なメソッド/ミックスイン

2つのモデル間に関連付けが定義されると、それらのモデルのインスタンスは、関連付けられた対応物と対話するための特別なメソッドを取得します。

たとえば、FooBarの2つのモデルがあり、それらが関連付けられている場合、それらのインスタンスは、関連付けタイプに応じて、次のメソッド/ミックスインを使用できるようになります。

Foo.hasOne(Bar)

  • fooInstance.getBar()
  • fooInstance.setBar()
  • fooInstance.createBar()

const foo = await Foo.create({ name: 'the-foo' });
const bar1 = await Bar.create({ name: 'some-bar' });
const bar2 = await Bar.create({ name: 'another-bar' });
console.log(await foo.getBar()); // null
await foo.setBar(bar1);
console.log((await foo.getBar()).name); // 'some-bar'
await foo.createBar({ name: 'yet-another-bar' });
const newlyAssociatedBar = await foo.getBar();
console.log(newlyAssociatedBar.name); // 'yet-another-bar'
await foo.setBar(null); // Un-associate
console.log(await foo.getBar()); // null

Foo.belongsTo(Bar)

Foo.hasOne(Bar)と同じもの

  • fooInstance.getBar()
  • fooInstance.setBar()
  • fooInstance.createBar()

Foo.hasMany(Bar)

  • fooInstance.getBars()
  • fooInstance.countBars()
  • fooInstance.hasBar()
  • fooInstance.hasBars()
  • fooInstance.setBars()
  • fooInstance.addBar()
  • fooInstance.addBars()
  • fooInstance.removeBar()
  • fooInstance.removeBars()
  • fooInstance.createBar()

const foo = await Foo.create({ name: 'the-foo' });
const bar1 = await Bar.create({ name: 'some-bar' });
const bar2 = await Bar.create({ name: 'another-bar' });
console.log(await foo.getBars()); // []
console.log(await foo.countBars()); // 0
console.log(await foo.hasBar(bar1)); // false
await foo.addBars([bar1, bar2]);
console.log(await foo.countBars()); // 2
await foo.addBar(bar1);
console.log(await foo.countBars()); // 2
console.log(await foo.hasBar(bar1)); // true
await foo.removeBar(bar2);
console.log(await foo.countBars()); // 1
await foo.createBar({ name: 'yet-another-bar' });
console.log(await foo.countBars()); // 2
await foo.setBars([]); // Un-associate all previously associated bars
console.log(await foo.countBars()); // 0

ゲッターメソッドは、通常のファインダーメソッド(findAllなど)と同じようにオプションを受け入れます。

const easyTasks = await project.getTasks({
where: {
difficulty: {
[Op.lte]: 5,
},
},
});
const taskTitles = (
await project.getTasks({
attributes: ['title'],
raw: true,
})
).map(task => task.title);

Foo.belongsToMany(Bar, { through: Baz })

Foo.hasMany(Bar) と同じものです。

  • fooInstance.getBars()
  • fooInstance.countBars()
  • fooInstance.hasBar()
  • fooInstance.hasBars()
  • fooInstance.setBars()
  • fooInstance.addBar()
  • fooInstance.addBars()
  • fooInstance.removeBar()
  • fooInstance.removeBars()
  • fooInstance.createBar()

belongsToManyの関係の場合、デフォルトではgetBars()は結合テーブルのすべてのフィールドを返します。includeオプションはターゲットのBarオブジェクトに適用されるため、findメソッドでEager Loadingを行うときのように、結合テーブルのオプションを設定することはできません。結合テーブルのどの属性を含めるかを選択するには、getBars()includethrough.attributesを設定するのと同様に使用できるjoinTableAttributesオプションをサポートしています。例として、FooがBarにbelongsToManyの場合、以下はいずれも結合テーブルのフィールドなしで結果を出力します。

const foo = Foo.findByPk(id, {
include: [
{
model: Bar,
through: { attributes: [] },
},
],
});
console.log(foo.bars);

const foo = Foo.findByPk(id);
console.log(foo.getBars({ joinTableAttributes: [] }));

注:メソッド名

上記の例で示したように、Sequelizeがこれらの特別なメソッドに付ける名前は、プレフィックス(例:getaddset)とモデル名(最初の文字は大文字)を連結して形成されます。必要に応じて、fooInstance.setBars()のように複数形が使用されます。繰り返しますが、不規則な複数形もSequelizeによって自動的に処理されます。たとえば、PersonPeopleになり、HypothesisHypothesesになります。

エイリアスが定義されている場合は、メソッド名を形成するためにモデル名の代わりに使用されます。たとえば、

Task.hasOne(User, { as: 'Author' });
  • taskInstance.getAuthor()
  • taskInstance.setAuthor()
  • taskInstance.createAuthor()

なぜ関連付けはペアで定義されるのですか?

前述のとおり、上記のほとんどの例で示されているように、通常、Sequelizeの関連付けはペアで定義されます。

  • **1対1** の関係を作成するには、hasOnebelongsTo の関連付けを一緒に使用します。
  • **1対多** の関係を作成するには、hasManybelongsTo の関連付けを一緒に使用します。
  • **多対多** の関係を作成するには、2 つの belongsToMany 呼び出しを一緒に使用します。

Sequelizeの関連付けが2つのモデル間で定義されている場合、*ソース* モデルのみが *それについて認識しています*。したがって、たとえばFoo.hasOne(Bar)(したがってFooがソースモデル、Barがターゲットモデル)を使用する場合、Fooのみがこの関連付けの存在を認識します。これが、この場合、上記のように、FooインスタンスがメソッドgetBar()setBar()createBar()を取得する一方、Barインスタンスは何も取得しない理由です。

同様に、Foo.hasOne(Bar)の場合、Fooが関係を認識しているため、Foo.findOne({ include: Bar })のようにEager Loadingを実行できますが、Bar.findOne({ include: Foo })を実行することはできません。

したがって、Sequelizeの使用を最大限に活用するために、通常、両方のモデルが *それを認識する* ようにペアで関係を設定します。

実践的なデモンストレーション

  • たとえば、Foo.hasOne(Bar)のみを呼び出して、関連付けのペアを定義しない場合

    // This works...
    await Foo.findOne({ include: Bar });

    // But this throws an error:
    await Bar.findOne({ include: Foo });
    // SequelizeEagerLoadingError: foo is not associated to bar!
  • 推奨どおりにペア(つまり、Foo.hasOne(Bar)Bar.belongsTo(Foo)の両方)を定義する場合

    // This works!
    await Foo.findOne({ include: Bar });

    // This also works!
    await Bar.findOne({ include: Foo });

同じモデルを含む複数の関連付け

Sequelizeでは、同じモデル間に複数の関連付けを定義することができます。それらに対して異なるエイリアスを定義するだけで済みます。

Team.hasOne(Game, { as: 'HomeTeam', foreignKey: 'homeTeamId' });
Team.hasOne(Game, { as: 'AwayTeam', foreignKey: 'awayTeamId' });
Game.belongsTo(Team);

主キーではないフィールドを参照する関連付けの作成

上記のすべての例では、関連付けは関係するモデルの主キー(この場合はID)を参照して定義されていました。ただし、Sequelizeでは、主キーフィールドの代わりに、関連付けを確立するために別のフィールドを使用する関連付けを定義できます。

この別のフィールドには一意制約が必要です(そうでないと意味がありません)。

belongsTo の関係の場合

まず、A.belongsTo(B)の関連付けは、外部キーを*ソースモデル*(つまり、A)に配置することを思い出してください。

再び、船長と船の例を使用しましょう。さらに、船長の名前は一意であると仮定します。

const Ship = sequelize.define('ship', { name: DataTypes.TEXT }, { timestamps: false });
const Captain = sequelize.define(
'captain',
{
name: { type: DataTypes.TEXT, unique: true },
},
{ timestamps: false },
);

このようにして、船にcaptainIdを保持する代わりに、代わりにcaptainNameを保持し、関連付けトラッカーとして使用できます。言い換えれば、ターゲットモデル(Captain)のidを参照する代わりに、関係はターゲットモデルの別の列、つまりname列を参照します。これを指定するには、*ターゲットキー*を定義する必要があります。外部キー自体の名前も指定する必要があります。

Ship.belongsTo(Captain, { targetKey: 'name', foreignKey: 'captainName' });
// This creates a foreign key called `captainName` in the source model (Ship)
// which references the `name` field from the target model (Captain).

これで、次のようなことを実行できます。

await Captain.create({ name: 'Jack Sparrow' });
const ship = await Ship.create({
name: 'Black Pearl',
captainName: 'Jack Sparrow',
});
console.log((await ship.getCaptain()).name); // "Jack Sparrow"

hasOne および hasMany の関係の場合

まったく同じ考え方をhasOneおよびhasManyの関連付けに適用できますが、関連付けを定義するときにtargetKeyを提供する代わりにsourceKeyを提供します。これは、belongsToとは異なり、hasOneおよびhasManyの関連付けは、外部キーをターゲットモデルに保持するためです。

const Foo = sequelize.define(
'foo',
{
name: { type: DataTypes.TEXT, unique: true },
},
{ timestamps: false },
);
const Bar = sequelize.define(
'bar',
{
title: { type: DataTypes.TEXT, unique: true },
},
{ timestamps: false },
);
const Baz = sequelize.define('baz', { summary: DataTypes.TEXT }, { timestamps: false });
Foo.hasOne(Bar, { sourceKey: 'name', foreignKey: 'fooName' });
Bar.hasMany(Baz, { sourceKey: 'title', foreignKey: 'barTitle' });
// [...]
await Bar.setFoo("Foo's Name Here");
await Baz.addBar("Bar's Title Here");

belongsToMany の関係の場合

同じ考え方をbelongsToManyの関係にも適用できます。ただし、外部キーが1つだけである他の状況とは異なり、belongsToManyの関係には、追加のテーブル(結合テーブル)に保持される2つの外部キーが含まれます。

次の設定を検討してください。

const Foo = sequelize.define(
'foo',
{
name: { type: DataTypes.TEXT, unique: true },
},
{ timestamps: false },
);
const Bar = sequelize.define(
'bar',
{
title: { type: DataTypes.TEXT, unique: true },
},
{ timestamps: false },
);

考慮すべき4つのケースがあります。

  • FooBarの両方のデフォルトの主キーを使用して、多対多の関係を望む場合があります。
Foo.belongsToMany(Bar, { through: 'foo_bar' });
// This creates a junction table `foo_bar` with fields `fooId` and `barId`
  • Fooのデフォルトの主キーを使用し、Barの別のフィールドを使用して、多対多の関係を望む場合があります。
Foo.belongsToMany(Bar, { through: 'foo_bar', targetKey: 'title' });
// This creates a junction table `foo_bar` with fields `fooId` and `barTitle`
  • Fooの別のフィールドを使用し、Barのデフォルトの主キーを使用して、多対多の関係を望む場合があります。
Foo.belongsToMany(Bar, { through: 'foo_bar', sourceKey: 'name' });
// This creates a junction table `foo_bar` with fields `fooName` and `barId`
  • FooBarの両方に異なるフィールドを使用して、多対多の関係を望む場合があります。
Foo.belongsToMany(Bar, {
through: 'foo_bar',
sourceKey: 'name',
targetKey: 'title',
});
// This creates a junction table `foo_bar` with fields `fooName` and `barTitle`

注記

関連付けで参照されるフィールドには、一意制約を配置する必要があることを忘れないでください。そうしないと、エラーがスローされます(場合によっては、SequelizeDatabaseError: SQLITE_ERROR: foreign key mismatch - "ships" referencing "captains"のような不可解なエラーメッセージが表示されます)。

sourceKeytargetKeyのどちらを選択するかを決定するためのコツは、各関係がどこに外部キーを配置するかを覚えておくことです。このガイドの冒頭で述べたように

  • A.belongsTo(B)は、外部キーをソースモデル(A)に保持するため、参照されるキーはターゲットモデルにあるため、targetKeyを使用します。

  • A.hasOne(B)およびA.hasMany(B)は、外部キーをターゲットモデル(B)に保持するため、参照されるキーはソースモデルにあるため、sourceKeyを使用します。

  • A.belongsToMany(B)は、追加のテーブル(結合テーブル)を含みます。したがって、sourceKeytargetKeyの両方が使用可能で、sourceKeyA(ソース)の一部のフィールドに対応し、targetKeyB(ターゲット)の一部のフィールドに対応します。