本文へスキップ
バージョン: v6 - 安定版

高度なM:N関連付け

このガイドを読む前に、関連付けガイド をお読みください。

UserProfile間の多対多関係の例から始めましょう。

const User = sequelize.define(
'user',
{
username: DataTypes.STRING,
points: DataTypes.INTEGER,
},
{ timestamps: false },
);
const Profile = sequelize.define(
'profile',
{
name: DataTypes.STRING,
},
{ timestamps: false },
);

多対多関係を定義する最も簡単な方法は、以下の通りです。

User.belongsToMany(Profile, { through: 'User_Profiles' });
Profile.belongsToMany(User, { through: 'User_Profiles' });

上記で`through`に文字列を渡すことで、Sequelizeは`User_Profiles`という名前のモデルを中間テーブル(ジャンクションテーブルとも呼ばれる)として自動的に生成するように指示しています。このテーブルには、`userId`と`profileId`の2つの列のみが含まれ、これら2つの列に複合一意キーが設定されます。

中間テーブルとして使用するモデルを自分で定義することもできます。

const User_Profile = sequelize.define('User_Profile', {}, { timestamps: false });
User.belongsToMany(Profile, { through: User_Profile });
Profile.belongsToMany(User, { through: User_Profile });

上記は全く同じ効果があります。`User_Profile`モデルには属性を定義していません。それを`belongsToMany`呼び出しに渡したという事実は、他の関連付けと同様に、Sequelizeが関連するモデルの1つに`userId`と`profileId`という2つの属性を自動的に作成することを意味します。

しかし、モデルを自分で定義することにはいくつかの利点があります。例えば、中間テーブルにさらに列を追加することができます。

const User_Profile = sequelize.define(
'User_Profile',
{
selfGranted: DataTypes.BOOLEAN,
},
{ timestamps: false },
);
User.belongsToMany(Profile, { through: User_Profile });
Profile.belongsToMany(User, { through: User_Profile });

これにより、中間テーブルに追加情報(`selfGranted`ブール値)を追跡できるようになります。例えば、`user.addProfile()`を呼び出す際に、`through`オプションを使用して追加列の値を渡すことができます。

const amidala = await User.create({ username: 'p4dm3', points: 1000 });
const queen = await Profile.create({ name: 'Queen' });
await amidala.addProfile(queen, { through: { selfGranted: false } });
const result = await User.findOne({
where: { username: 'p4dm3' },
include: Profile,
});
console.log(result);

出力

{
"id": 4,
"username": "p4dm3",
"points": 1000,
"profiles": [
{
"id": 6,
"name": "queen",
"User_Profile": {
"userId": 4,
"profileId": 6,
"selfGranted": false
}
}
]
}

すべての関係を単一の`create`呼び出しで作成することもできます。

const amidala = await User.create(
{
username: 'p4dm3',
points: 1000,
profiles: [
{
name: 'Queen',
User_Profile: {
selfGranted: true,
},
},
],
},
{
include: Profile,
},
);

const result = await User.findOne({
where: { username: 'p4dm3' },
include: Profile,
});

console.log(result);

出力

{
"id": 1,
"username": "p4dm3",
"points": 1000,
"profiles": [
{
"id": 1,
"name": "Queen",
"User_Profile": {
"selfGranted": true,
"userId": 1,
"profileId": 1
}
}
]
}

`User_Profiles`テーブルに`id`フィールドがないことに気付かれたかもしれません。上記のように、代わりに複合一意キーを持っています。この複合一意キーの名前はSequelizeによって自動的に選択されますが、`uniqueKey`オプションでカスタマイズできます。

User.belongsToMany(Profile, {
through: User_Profiles,
uniqueKey: 'my_custom_unique',
});

別の方法として、他の標準テーブルと同様に、中間テーブルに主キーを強制的に持たせることもできます。これを行うには、モデルで主キーを定義するだけです。

const User_Profile = sequelize.define(
'User_Profile',
{
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
allowNull: false,
},
selfGranted: DataTypes.BOOLEAN,
},
{ timestamps: false },
);
User.belongsToMany(Profile, { through: User_Profile });
Profile.belongsToMany(User, { through: User_Profile });

上記でも、`userId`と`profileId`の2つの列は作成されますが、それらに複合一意キーを設定する代わりに、モデルは`id`列を主キーとして使用します。その他はすべて正常に動作します。

中間テーブルと通常のテーブル、そして「スーパー多対多関連付け」

ここでは、上記で示した最後の多対多設定の使用法を、通常の単対多関係と比較することで、「スーパー多対多関係」の概念を最終的に導き出します。

モデルの要約(名前の変更あり)

分かりやすくするために、`User_Profile`モデルの名前を`grant`に変更します。すべて以前と同じように動作します。モデルは以下の通りです。

const User = sequelize.define(
'user',
{
username: DataTypes.STRING,
points: DataTypes.INTEGER,
},
{ timestamps: false },
);

const Profile = sequelize.define(
'profile',
{
name: DataTypes.STRING,
},
{ timestamps: false },
);

const Grant = sequelize.define(
'grant',
{
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
allowNull: false,
},
selfGranted: DataTypes.BOOLEAN,
},
{ timestamps: false },
);

中間テーブルとして`Grant`モデルを使用して、`User`と`Profile`の間に多対多関係を確立しました。

User.belongsToMany(Profile, { through: Grant });
Profile.belongsToMany(User, { through: Grant });

これにより、`Grant`モデルに`userId`と`profileId`の列が自動的に追加されました。

**注:** 上記のように、`grant`モデルに単一の主キー(通常は`id`)を強制的に持たせるように選択しました。これは、すぐに定義されるスーパー多対多関係に必要です。

代わりに単対多関係を使用する

上記で定義した多対多関係を設定する代わりに、代わりに以下のようにした場合を考えてみましょう。

// Setup a One-to-Many relationship between User and Grant
User.hasMany(Grant);
Grant.belongsTo(User);

// Also setup a One-to-Many relationship between Profile and Grant
Profile.hasMany(Grant);
Grant.belongsTo(Profile);

結果は本質的に同じです!これは、`User.hasMany(Grant)`と`Profile.hasMany(Grant)`がそれぞれ`userId`と`profileId`列を`Grant`に自動的に追加するためです。

これは、1つの多対多関係が2つの単対多関係とそれほど変わらないことを示しています。データベースのテーブルは同じように見えます。

違いは、SequelizeでEager Loadingを実行しようとしたときだけです。

// With the Many-to-Many approach, you can do:
User.findAll({ include: Profile });
Profile.findAll({ include: User });
// However, you can't do:
User.findAll({ include: Grant });
Profile.findAll({ include: Grant });
Grant.findAll({ include: User });
Grant.findAll({ include: Profile });

// On the other hand, with the double One-to-Many approach, you can do:
User.findAll({ include: Grant });
Profile.findAll({ include: Grant });
Grant.findAll({ include: User });
Grant.findAll({ include: Profile });
// However, you can't do:
User.findAll({ include: Profile });
Profile.findAll({ include: User });
// Although you can emulate those with nested includes, as follows:
User.findAll({
include: {
model: Grant,
include: Profile,
},
}); // This emulates the `User.findAll({ include: Profile })`, however
// the resulting object structure is a bit different. The original
// structure has the form `user.profiles[].grant`, while the emulated
// structure has the form `user.grants[].profiles[]`.

両方の長所を兼ね備えたもの:スーパー多対多関係

上記で示した両方のアプローチを組み合わせるだけです!

// The Super Many-to-Many relationship
User.belongsToMany(Profile, { through: Grant });
Profile.belongsToMany(User, { through: Grant });
User.hasMany(Grant);
Grant.belongsTo(User);
Profile.hasMany(Grant);
Grant.belongsTo(Profile);

このようにして、あらゆる種類のEager Loadingを行うことができます。

// All these work:
User.findAll({ include: Profile });
Profile.findAll({ include: User });
User.findAll({ include: Grant });
Profile.findAll({ include: Grant });
Grant.findAll({ include: User });
Grant.findAll({ include: Profile });

あらゆる種類の深くネストされたインクルードを実行することもできます。

User.findAll({
include: [
{
model: Grant,
include: [User, Profile],
},
{
model: Profile,
include: {
model: User,
include: {
model: Grant,
include: [User, Profile],
},
},
},
],
});

エイリアスとカスタムキー名

他の関係と同様に、多対多関係にもエイリアスを定義できます。

先に進む前に、`belongsTo`のエイリアスの例関連付けガイドで思い出してください。その場合、関連付けを定義すると、インクルードの方法(つまり、関連付けの名前を渡すこと)と、Sequelizeが外部キーに選択する名前の両方に影響します(その例では、`leaderId`が`Ship`モデルに作成されました)。

`belongsToMany`関連付けのエイリアスを定義すると、インクルードの実行方法にも影響します。

Product.belongsToMany(Category, {
as: 'groups',
through: 'product_categories',
});
Category.belongsToMany(Product, { as: 'items', through: 'product_categories' });

// [...]

await Product.findAll({ include: Category }); // This doesn't work

await Product.findAll({
// This works, passing the alias
include: {
model: Category,
as: 'groups',
},
});

await Product.findAll({ include: 'groups' }); // This also works

しかし、ここでエイリアスを定義することは、外部キー名とは関係ありません。中間テーブルに作成される2つの外部キーの名前は、関連付けられているモデルの名前に基づいてSequelizeによって構築されます。これは、上記の例の中間テーブルで生成されたSQLを検査することで簡単に確認できます。

CREATE TABLE IF NOT EXISTS `product_categories` (
`createdAt` DATETIME NOT NULL,
`updatedAt` DATETIME NOT NULL,
`productId` INTEGER NOT NULL REFERENCES `products` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
`categoryId` INTEGER NOT NULL REFERENCES `categories` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
PRIMARY KEY (`productId`, `categoryId`)
);

外部キーは`productId`と`categoryId`であることが分かります。これらの名前を変更するには、Sequelizeはそれぞれ`foreignKey`と`otherKey`オプションを受け入れます(つまり、`foreignKey`は中間関係におけるソースモデルのキーを定義し、`otherKey`はターゲットモデルのキーを定義します)。

Product.belongsToMany(Category, {
through: 'product_categories',
foreignKey: 'objectId', // replaces `productId`
otherKey: 'typeId', // replaces `categoryId`
});
Category.belongsToMany(Product, {
through: 'product_categories',
foreignKey: 'typeId', // replaces `categoryId`
otherKey: 'objectId', // replaces `productId`
});

生成されたSQL

CREATE TABLE IF NOT EXISTS `product_categories` (
`createdAt` DATETIME NOT NULL,
`updatedAt` DATETIME NOT NULL,
`objectId` INTEGER NOT NULL REFERENCES `products` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
`typeId` INTEGER NOT NULL REFERENCES `categories` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
PRIMARY KEY (`objectId`, `typeId`)
);

上記のように、2つの`belongsToMany`呼び出しで多対多関係を定義する場合(標準的な方法です)、両方の呼び出しで`foreignKey`と`otherKey`オプションを適切に指定する必要があります。これらのオプションを1つの呼び出しでのみ渡すと、Sequelizeの動作は信頼できなくなります。

自己参照

Sequelizeは、直感的に自己参照的な多対多関係をサポートしています。

Person.belongsToMany(Person, { as: 'Children', through: 'PersonChildren' });
// This will create the table PersonChildren which stores the ids of the objects.

中間テーブルからの属性の指定

デフォルトでは、多対多関係をEager Loadingすると、Sequelizeは次の構造のデータ(このガイドの最初の例に基づく)を返します。

// User.findOne({ include: Profile })
{
"id": 4,
"username": "p4dm3",
"points": 1000,
"profiles": [
{
"id": 6,
"name": "queen",
"grant": {
"userId": 4,
"profileId": 6,
"selfGranted": false
}
}
]
}

外側のオブジェクトは`User`であり、`profiles`というフィールドがあります。これは`Profile`の配列であり、各`Profile`には`grant`という追加のフィールドがあり、これは`Grant`インスタンスです。これは、多対多関係からEager LoadingしたときにSequelizeによって作成されるデフォルトの構造です。

しかし、中間テーブルの一部の属性のみが必要な場合は、必要な属性を含む配列を`attributes`オプションに指定できます。例えば、中間テーブルの`selfGranted`属性のみが必要な場合は、次のようになります。

User.findOne({
include: {
model: Profile,
through: {
attributes: ['selfGranted'],
},
},
});

出力

{
"id": 4,
"username": "p4dm3",
"points": 1000,
"profiles": [
{
"id": 6,
"name": "queen",
"grant": {
"selfGranted": false
}
}
]
}

ネストされた`grant`フィールドを全く必要としない場合は、`attributes: []`を使用します。

User.findOne({
include: {
model: Profile,
through: {
attributes: [],
},
},
});

出力

{
"id": 4,
"username": "p4dm3",
"points": 1000,
"profiles": [
{
"id": 6,
"name": "queen"
}
]
}

ファインダーメソッド(`User.findAll()`など)ではなくミキシン(`user.getProfiles()`など)を使用している場合は、代わりに`joinTableAttributes`オプションを使用する必要があります。

someUser.getProfiles({ joinTableAttributes: ['selfGranted'] });

出力

[
{
"id": 6,
"name": "queen",
"grant": {
"selfGranted": false
}
}
]

多対多対多関係など

ゲームの選手権をモデル化しようとしているとします。選手とチームがあります。チームはゲームをします。しかし、選手は選手権の途中でチームを変更できます(ゲームの途中で変更することはできません)。したがって、特定のゲームを考えると、そのゲームに参加している特定のチームがあり、これらのチームにはそれぞれ(そのゲームの)選手がいます。

そのため、関連する3つのモデルを定義することから始めます。

const Player = sequelize.define('Player', { username: DataTypes.STRING });
const Team = sequelize.define('Team', { name: DataTypes.STRING });
const Game = sequelize.define('Game', { name: DataTypes.STRING });

問題は、それらをどのように関連付けるかです。

まず、以下のことに注意します。

  • 1つのゲームには、それに関連付けられた多くのチームがあります(そのゲームをしているチーム)。
  • 1つのチームは、多くのゲームに参加している可能性があります。

上記の観察結果から、ゲームとチームの間に多対多の関係が必要であることがわかります。本ガイドの前の方で説明したスーパー多対多関係を使用しましょう。

// Super Many-to-Many relationship between Game and Team
const GameTeam = sequelize.define('GameTeam', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
allowNull: false,
},
});
Team.belongsToMany(Game, { through: GameTeam });
Game.belongsToMany(Team, { through: GameTeam });
GameTeam.belongsTo(Game);
GameTeam.belongsTo(Team);
Game.hasMany(GameTeam);
Team.hasMany(GameTeam);

プレイヤーに関する部分はより複雑です。チームを構成するプレイヤーの集合は、チーム自体(明らかに)だけでなく、どのゲームを考慮しているかにも依存することに注意してください。そのため、プレイヤーとチームの間に多対多の関係は望ましくありません。プレイヤーとゲームの間に多対多の関係も望ましくありません。これらのモデルのいずれかにプレイヤーを関連付ける代わりに、必要なのはプレイヤーと「チームとゲームのペア制約」のようなものとの関連付けです。なぜなら、(チームとゲームの)ペアによって、どのプレイヤーがそこに所属するかが定義されるからです。したがって、私たちが探しているものは、まさに接合モデルであるGameTeamそのものなのです!そして、特定のゲームとチームのペアは多くのプレイヤーを指定し、一方で同じプレイヤーは多くのゲームとチームのペアに参加できるため、プレイヤーとGameTeamの間に多対多の関係が必要になります!

最大の柔軟性を提供するために、ここで再びスーパー多対多関係構造を使用しましょう。

// Super Many-to-Many relationship between Player and GameTeam
const PlayerGameTeam = sequelize.define('PlayerGameTeam', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
allowNull: false,
},
});
Player.belongsToMany(GameTeam, { through: PlayerGameTeam });
GameTeam.belongsToMany(Player, { through: PlayerGameTeam });
PlayerGameTeam.belongsTo(Player);
PlayerGameTeam.belongsTo(GameTeam);
Player.hasMany(PlayerGameTeam);
GameTeam.hasMany(PlayerGameTeam);

上記の関連付けにより、まさに私たちが望むものが実現します。これがその完全な実行可能な例です。

const { Sequelize, Op, Model, DataTypes } = require('sequelize');
const sequelize = new Sequelize('sqlite::memory:', {
define: { timestamps: false }, // Just for less clutter in this example
});
const Player = sequelize.define('Player', { username: DataTypes.STRING });
const Team = sequelize.define('Team', { name: DataTypes.STRING });
const Game = sequelize.define('Game', { name: DataTypes.STRING });

// We apply a Super Many-to-Many relationship between Game and Team
const GameTeam = sequelize.define('GameTeam', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
allowNull: false,
},
});
Team.belongsToMany(Game, { through: GameTeam });
Game.belongsToMany(Team, { through: GameTeam });
GameTeam.belongsTo(Game);
GameTeam.belongsTo(Team);
Game.hasMany(GameTeam);
Team.hasMany(GameTeam);

// We apply a Super Many-to-Many relationship between Player and GameTeam
const PlayerGameTeam = sequelize.define('PlayerGameTeam', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
allowNull: false,
},
});
Player.belongsToMany(GameTeam, { through: PlayerGameTeam });
GameTeam.belongsToMany(Player, { through: PlayerGameTeam });
PlayerGameTeam.belongsTo(Player);
PlayerGameTeam.belongsTo(GameTeam);
Player.hasMany(PlayerGameTeam);
GameTeam.hasMany(PlayerGameTeam);

(async () => {
await sequelize.sync();
await Player.bulkCreate([
{ username: 's0me0ne' },
{ username: 'empty' },
{ username: 'greenhead' },
{ username: 'not_spock' },
{ username: 'bowl_of_petunias' },
]);
await Game.bulkCreate([
{ name: 'The Big Clash' },
{ name: 'Winter Showdown' },
{ name: 'Summer Beatdown' },
]);
await Team.bulkCreate([
{ name: 'The Martians' },
{ name: 'The Earthlings' },
{ name: 'The Plutonians' },
]);

// Let's start defining which teams were in which games. This can be done
// in several ways, such as calling `.setTeams` on each game. However, for
// brevity, we will use direct `create` calls instead, referring directly
// to the IDs we want. We know that IDs are given in order starting from 1.
await GameTeam.bulkCreate([
{ GameId: 1, TeamId: 1 }, // this GameTeam will get id 1
{ GameId: 1, TeamId: 2 }, // this GameTeam will get id 2
{ GameId: 2, TeamId: 1 }, // this GameTeam will get id 3
{ GameId: 2, TeamId: 3 }, // this GameTeam will get id 4
{ GameId: 3, TeamId: 2 }, // this GameTeam will get id 5
{ GameId: 3, TeamId: 3 }, // this GameTeam will get id 6
]);

// Now let's specify players.
// For brevity, let's do it only for the second game (Winter Showdown).
// Let's say that that s0me0ne and greenhead played for The Martians, while
// not_spock and bowl_of_petunias played for The Plutonians:
await PlayerGameTeam.bulkCreate([
// In 'Winter Showdown' (i.e. GameTeamIds 3 and 4):
{ PlayerId: 1, GameTeamId: 3 }, // s0me0ne played for The Martians
{ PlayerId: 3, GameTeamId: 3 }, // greenhead played for The Martians
{ PlayerId: 4, GameTeamId: 4 }, // not_spock played for The Plutonians
{ PlayerId: 5, GameTeamId: 4 }, // bowl_of_petunias played for The Plutonians
]);

// Now we can make queries!
const game = await Game.findOne({
where: {
name: 'Winter Showdown',
},
include: {
model: GameTeam,
include: [
{
model: Player,
through: { attributes: [] }, // Hide unwanted `PlayerGameTeam` nested object from results
},
Team,
],
},
});

console.log(`Found game: "${game.name}"`);
for (let i = 0; i < game.GameTeams.length; i++) {
const team = game.GameTeams[i].Team;
const players = game.GameTeams[i].Players;
console.log(`- Team "${team.name}" played game "${game.name}" with the following players:`);
console.log(players.map(p => `--- ${p.username}`).join('\n'));
}
})();

出力

Found game: "Winter Showdown"
- Team "The Martians" played game "Winter Showdown" with the following players:
--- s0me0ne
--- greenhead
- Team "The Plutonians" played game "Winter Showdown" with the following players:
--- not_spock
--- bowl_of_petunias

このように、スーパー多対多関係テクニックを利用することで、Sequelizeで3つのモデル間の多対多対多の関係を実現できます!

この考え方は、さらに複雑な多対多対…対多の関係に再帰的に適用できます(ただし、ある時点でクエリが遅くなる可能性があります)。