Eager Loading
アソシエーションガイドで簡単に説明したように、Eager Loadingとは、複数のモデルのデータを一度にクエリする行為です(1つの「メイン」モデルと1つ以上の関連付けられたモデル)。SQLレベルでは、これは1つ以上のJOINを使用したクエリです。
これを実行すると、関連付けられたモデルは、Sequelizeによって、返されるオブジェクト内の適切に名前付けされた、自動的に作成されたフィールドに追加されます。
Sequelizeでは、Eager Loadingは主に、モデルファインダークエリ(findOne
、findAll
など)のinclude
オプションを使用して実行されます。
基本例
次の設定を想定します
const User = sequelize.define('user', { name: DataTypes.STRING }, { timestamps: false });
const Task = sequelize.define('task', { name: DataTypes.STRING }, { timestamps: false });
const Tool = sequelize.define(
'tool',
{
name: DataTypes.STRING,
size: DataTypes.STRING,
},
{ timestamps: false },
);
User.hasMany(Task);
Task.belongsTo(User);
User.hasMany(Tool, { as: 'Instruments' });
関連付けられた単一要素の取得
まず、関連付けられたユーザーを含むすべてのタスクを読み込んでみましょう
const tasks = await Task.findAll({ include: User });
console.log(JSON.stringify(tasks, null, 2));
出力
[
{
"name": "A Task",
"id": 1,
"userId": 1,
"user": {
"name": "John Doe",
"id": 1
}
}
]
ここで、tasks[0].user instanceof User
はtrue
です。これは、Sequelizeが関連付けられたモデルを取得すると、それらがモデルインスタンスとして出力オブジェクトに追加されることを示しています。
上記では、関連付けられたモデルは、取得されたタスクのuser
という新しいフィールドに追加されました。このフィールドの名前は、関連付けられたモデルの名前(アソシエーションがhasMany
またはbelongsToMany
の場合、その複数形が使用されます)に基づいて、Sequelizeによって自動的に選択されました。言い換えれば、Task.belongsTo(User)
なので、タスクは1人のユーザーに関連付けられているため、論理的な選択は単数形です(Sequelizeは自動的にこれに従います)。
関連付けられたすべての要素の取得
今度は、特定のタスクに関連付けられているユーザーを読み込む代わりに、逆を行います。つまり、特定のユーザーに関連付けられているすべてのタスクを見つけます。
メソッド呼び出しは基本的に同じです。唯一の違いは、クエリ結果で作成された追加フィールドが複数形(この場合はtasks
)を使用し、その値がタスクインスタンスの配列(上記のように単一のインスタンスではなく)であることです。
const users = await User.findAll({ include: Task });
console.log(JSON.stringify(users, null, 2));
出力
[
{
"name": "John Doe",
"id": 1,
"tasks": [
{
"name": "A Task",
"id": 1,
"userId": 1
}
]
}
]
アソシエーションが1対多であるため、アクセサ(結果のインスタンスのtasks
プロパティ)は複数形であることに注意してください。
エイリアスされたアソシエーションの取得
アソシエーションにエイリアスが付けられている場合(as
オプションを使用)、モデルを含める際にこのエイリアスを指定する必要があります。モデルをinclude
オプションに直接渡す代わりに、model
とas
の2つのオプションを持つオブジェクトを提供する必要があります。
上記のユーザーのTool
はInstruments
としてエイリアスされていることに注意してください。正しく取得するには、ロードするモデルとエイリアスの両方を指定する必要があります。
const users = await User.findAll({
include: { model: Tool, as: 'Instruments' },
});
console.log(JSON.stringify(users, null, 2));
出力
[
{
"name": "John Doe",
"id": 1,
"Instruments": [
{
"name": "Scissor",
"id": 1,
"userId": 1
}
]
}
]
アソシエーションのエイリアス名と一致する文字列を指定することで、エイリアス名で含めることもできます。
User.findAll({ include: 'Instruments' }); // Also works
User.findAll({ include: { association: 'Instruments' } }); // Also works
必須Eager Loading
Eager Loadingでは、関連付けられたモデルを持つレコードのみを返すようにクエリを強制し、クエリをデフォルトのOUTER JOIN
からINNER JOIN
に効果的に変換できます。これは、次のようにrequired: true
オプションを使用することで実行されます。
User.findAll({
include: {
model: Task,
required: true,
},
});
このオプションは、ネストされたincludeでも機能します。
関連付けられたモデルレベルでフィルタリングされたEager Loading
Eager Loadingでは、次の例のようにwhere
オプションを使用して関連付けられたモデルをフィルタリングすることもできます。
User.findAll({
include: {
model: Tool,
as: 'Instruments',
where: {
size: {
[Op.ne]: 'small',
},
},
},
});
生成されたSQL
SELECT
`user`.`id`,
`user`.`name`,
`Instruments`.`id` AS `Instruments.id`,
`Instruments`.`name` AS `Instruments.name`,
`Instruments`.`size` AS `Instruments.size`,
`Instruments`.`userId` AS `Instruments.userId`
FROM `users` AS `user`
INNER JOIN `tools` AS `Instruments` ON
`user`.`id` = `Instruments`.`userId` AND
`Instruments`.`size` != 'small';
上記の生成されたSQLクエリは、少なくとも1つの条件に一致するツールを持つユーザーのみを取得することに注意してください(この場合はsmall
ではない)。これは、where
オプションがinclude
内で使用されると、Sequelizeが自動的にrequired
オプションをtrue
に設定するためです。つまり、OUTER JOIN
の代わりにINNER JOIN
が行われ、少なくとも1つの一致する子を持つ親モデルのみが返されます。
また、使用されたwhere
オプションは、INNER JOIN
のON
句の条件に変換されたことにも注意してください。ON
句ではなく、トップレベルのWHERE
句を取得するには、別の方法を行う必要があります。これは次に示します。
他の列への参照
関連付けられたモデルの値を参照するincludeされたモデルにWHERE
句を適用したい場合は、次の例に示すようにSequelize.col
関数を使用するだけで済みます。
// Find all projects with a least one task where task.state === project.state
Project.findAll({
include: {
model: Task,
where: {
state: Sequelize.col('project.state'),
},
},
});
トップレベルの複雑なWHERE句
ネストされた列を含むトップレベルのWHERE
句を取得するために、Sequelizeはネストされた列を参照する方法を提供します。それは'$nested.column$'
構文です。
これは、たとえば、includeされたモデルのwhere条件をON
条件からトップレベルのWHERE
句に移動するために使用できます。
User.findAll({
where: {
'$Instruments.size$': { [Op.ne]: 'small' },
},
include: [
{
model: Tool,
as: 'Instruments',
},
],
});
生成されたSQL
SELECT
`user`.`id`,
`user`.`name`,
`Instruments`.`id` AS `Instruments.id`,
`Instruments`.`name` AS `Instruments.name`,
`Instruments`.`size` AS `Instruments.size`,
`Instruments`.`userId` AS `Instruments.userId`
FROM `users` AS `user`
LEFT OUTER JOIN `tools` AS `Instruments` ON
`user`.`id` = `Instruments`.`userId`
WHERE `Instruments`.`size` != 'small';
$nested.column$
構文は、$some.super.deeply.nested.column$
など、複数レベルにネストされた列にも機能します。したがって、これを使用して、深くネストされた列に複雑なフィルタを適用できます。
required
オプションの有無にかかわらず、include
内で使用される内部where
オプションと、$nested.column$
構文を使用したトップレベルのwhere
とのすべての違いをよりよく理解するために、以下に4つの例を示します。
// Inner where, with default `required: true`
await User.findAll({
include: {
model: Tool,
as: 'Instruments',
where: {
size: { [Op.ne]: 'small' },
},
},
});
// Inner where, `required: false`
await User.findAll({
include: {
model: Tool,
as: 'Instruments',
where: {
size: { [Op.ne]: 'small' },
},
required: false,
},
});
// Top-level where, with default `required: false`
await User.findAll({
where: {
'$Instruments.size$': { [Op.ne]: 'small' },
},
include: {
model: Tool,
as: 'Instruments',
},
});
// Top-level where, `required: true`
await User.findAll({
where: {
'$Instruments.size$': { [Op.ne]: 'small' },
},
include: {
model: Tool,
as: 'Instruments',
required: true,
},
});
順番に生成されたSQL
-- Inner where, with default `required: true`
SELECT [...] FROM `users` AS `user`
INNER JOIN `tools` AS `Instruments` ON
`user`.`id` = `Instruments`.`userId`
AND `Instruments`.`size` != 'small';
-- Inner where, `required: false`
SELECT [...] FROM `users` AS `user`
LEFT OUTER JOIN `tools` AS `Instruments` ON
`user`.`id` = `Instruments`.`userId`
AND `Instruments`.`size` != 'small';
-- Top-level where, with default `required: false`
SELECT [...] FROM `users` AS `user`
LEFT OUTER JOIN `tools` AS `Instruments` ON
`user`.`id` = `Instruments`.`userId`
WHERE `Instruments`.`size` != 'small';
-- Top-level where, `required: true`
SELECT [...] FROM `users` AS `user`
INNER JOIN `tools` AS `Instruments` ON
`user`.`id` = `Instruments`.`userId`
WHERE `Instruments`.`size` != 'small';
RIGHT OUTER JOIN
を使用したフェッチ(MySQL、MariaDB、PostgreSQL、MSSQLのみ)
デフォルトでは、アソシエーションはLEFT OUTER JOIN
を使用してロードされます。つまり、親テーブルのレコードのみが含まれます。使用しているDialectがサポートしている場合、right
オプションを渡すことで、この動作をRIGHT OUTER JOIN
に変更できます。
現在、SQLiteはライトジョインをサポートしていません。
注記:right
は、required
がfalseの場合にのみ尊重されます。
User.findAll({
include: [{
model: Task // will create a left join
}]
});
User.findAll({
include: [{
model: Task,
right: true // will create a right join
}]
});
User.findAll({
include: [{
model: Task,
required: true,
right: true // has no effect, will create an inner join
}]
});
User.findAll({
include: [{
model: Task,
where: { name: { [Op.ne]: 'empty trash' } },
right: true // has no effect, will create an inner join
}]
});
User.findAll({
include: [{
model: Tool,
where: { name: { [Op.ne]: 'empty trash' } },
required: false // will create a left join
}]
});
User.findAll({
include: [{
model: Tool,
where: { name: { [Op.ne]: 'empty trash' } },
required: false
right: true // will create a right join
}]
});
複数Eager Loading
include
オプションは、複数の関連付けられたモデルを一度にフェッチするために配列を受け取ることができます。
Foo.findAll({
include: [
{
model: Bar,
required: true
},
{
model: Baz,
where: /* ... */
},
Qux // Shorthand syntax for { model: Qux } also works here
]
})
多対多リレーションシップでのEager Loading
多対多リレーションシップを持つモデルでEager Loadingを実行すると、デフォルトでSequelizeはジャンクションテーブルデータも取得します。例えば
const Foo = sequelize.define('Foo', { name: DataTypes.TEXT });
const Bar = sequelize.define('Bar', { name: DataTypes.TEXT });
Foo.belongsToMany(Bar, { through: 'Foo_Bar' });
Bar.belongsToMany(Foo, { through: 'Foo_Bar' });
await sequelize.sync();
const foo = await Foo.create({ name: 'foo' });
const bar = await Bar.create({ name: 'bar' });
await foo.addBar(bar);
const fetchedFoo = await Foo.findOne({ include: Bar });
console.log(JSON.stringify(fetchedFoo, null, 2));
出力
{
"id": 1,
"name": "foo",
"Bars": [
{
"id": 1,
"name": "bar",
"Foo_Bar": {
"FooId": 1,
"BarId": 1
}
}
]
}
Eager Loadingされたすべてのbarインスタンスの"Bars"
プロパティには、関連するSequelizeのジャンクションモデルインスタンスであるFoo_Bar
という追加のプロパティがあることに注意してください。デフォルトでは、Sequelizeはこの追加のプロパティを作成するために、ジャンクションテーブルからすべての属性を取得します。
ただし、取得する属性を指定できます。これは、includeのthrough
オプションに適用されたattributes
オプションを使用して行います。例えば
Foo.findAll({
include: [
{
model: Bar,
through: {
attributes: [
/* list the wanted attributes here */
],
},
},
],
});
ジャンクションテーブルから何も取得したくない場合は、include
オプションのthrough
オプション内のattributes
オプションに空の配列を明示的に指定できます。この場合、何も取得されず、追加のプロパティは作成されません。
Foo.findOne({
include: {
model: Bar,
through: {
attributes: [],
},
},
});
出力
{
"id": 1,
"name": "foo",
"Bars": [
{
"id": 1,
"name": "bar"
}
]
}
多対多リレーションシップからモデルを含める際は、ジャンクションテーブルにフィルターを適用することもできます。これは、include
オプション内のwhere
オプションを使用して行います。例:
User.findAll({
include: [
{
model: Project,
through: {
where: {
// Here, `completed` is a column present at the junction table
completed: true,
},
},
},
],
});
生成されたSQL (SQLite使用)
SELECT
`User`.`id`,
`User`.`name`,
`Projects`.`id` AS `Projects.id`,
`Projects`.`name` AS `Projects.name`,
`Projects->User_Project`.`completed` AS `Projects.User_Project.completed`,
`Projects->User_Project`.`UserId` AS `Projects.User_Project.UserId`,
`Projects->User_Project`.`ProjectId` AS `Projects.User_Project.ProjectId`
FROM `Users` AS `User`
LEFT OUTER JOIN `User_Projects` AS `Projects->User_Project` ON
`User`.`id` = `Projects->User_Project`.`UserId`
LEFT OUTER JOIN `Projects` AS `Projects` ON
`Projects`.`id` = `Projects->User_Project`.`ProjectId` AND
`Projects->User_Project`.`completed` = 1;
すべてを含める
関連付けられたすべてのモデルを含めるには、all
およびnested
オプションを使用できます。
// Fetch all models associated with User
User.findAll({ include: { all: true } });
// Fetch all models associated with User and their nested associations (recursively)
User.findAll({ include: { all: true, nested: true } });
ソフト削除されたレコードを含める
ソフト削除されたレコードを eager load したい場合は、include.paranoid
をfalse
に設定します。
User.findAll({
include: [
{
model: Tool,
as: 'Instruments',
where: { size: { [Op.ne]: 'small' } },
paranoid: false,
},
],
});
eager loadされた関連付けのソート
eager loadされたモデルにORDER
句を適用する場合は、ソートしたいネストされたモデルの指定から始まる拡張配列を使用して、最上位レベルのorder
オプションを使用する必要があります。
これは、例で理解しやすくなります。
Company.findAll({
include: Division,
order: [
// We start the order array with the model we want to sort
[Division, 'name', 'ASC'],
],
});
Company.findAll({
include: Division,
order: [[Division, 'name', 'DESC']],
});
Company.findAll({
// If the include uses an alias...
include: { model: Division, as: 'Div' },
order: [
// ...we use the same syntax from the include
// in the beginning of the order array
[{ model: Division, as: 'Div' }, 'name', 'DESC'],
],
});
Company.findAll({
// If we have includes nested in several levels...
include: {
model: Division,
include: Department,
},
order: [
// ... we replicate the include chain of interest
// at the beginning of the order array
[Division, Department, 'name', 'DESC'],
],
});
多対多リレーションシップの場合、throughテーブルの属性でソートすることもできます。たとえば、Division
とDepartment
の間にジャンクションモデルがDepartmentDivision
である多対多リレーションシップがあると仮定すると、次のようにできます。
Company.findAll({
include: {
model: Division,
include: Department,
},
order: [[Division, DepartmentDivision, 'name', 'ASC']],
});
上記のすべての例で、order
オプションが最上位レベルで使用されていることに気付かれたと思います。include
オプション内でorder
が機能する唯一の状況は、separate: true
が使用されている場合です。その場合、使い方は次のとおりです。
// This only works for `separate: true` (which in turn
// only works for has-many relationships).
User.findAll({
include: {
model: Post,
separate: true,
order: [['createdAt', 'DESC']],
},
});
サブクエリを含む複雑なソート
より複雑なソートを支援するためのサブクエリの使い方の例については、サブクエリに関するガイドを参照してください。
ネストされた eager loading
ネストされた eager loadingを使用して、関連モデルのすべての関連モデルをロードできます。
const users = await User.findAll({
include: {
model: Tool,
as: 'Instruments',
include: {
model: Teacher,
include: [
/* etc */
],
},
},
});
console.log(JSON.stringify(users, null, 2));
出力
[
{
"name": "John Doe",
"id": 1,
"Instruments": [
{
// 1:M and N:M association
"name": "Scissor",
"id": 1,
"userId": 1,
"Teacher": {
// 1:1 association
"name": "Jimi Hendrix"
}
}
]
}
]
これにより、外部結合が生成されます。ただし、関連モデルのwhere
句は内部結合を作成し、一致するサブモデルを持つインスタンスのみを返します。すべての親インスタンスを返すには、required: false
を追加する必要があります。
User.findAll({
include: [
{
model: Tool,
as: 'Instruments',
include: [
{
model: Teacher,
where: {
school: 'Woodstock Music School',
},
required: false,
},
],
},
],
});
上記のクエリは、すべてのユーザーとその楽器をすべて返しますが、「Woodstock Music School」に関連付けられている教師のみを返します。
include
を使用したfindAndCountAll
の使用
findAndCountAll
ユーティリティ関数はinclude
をサポートします。required
としてマークされたinclude
のみがcount
で考慮されます。たとえば、プロフィールを持つすべてのユーザーを見つけてカウントしたい場合
User.findAndCountAll({
include: [{ model: Profile, required: true }],
limit: 3,
});
Profile
のinclude
にrequired
が設定されているため、内部結合になり、プロフィールを持つユーザーのみがカウントされます。include
からrequired
を削除すると、プロフィールのあるユーザーとないユーザーの両方がカウントされます。include
にwhere
句を追加すると、自動的にrequired
がtrueになります。
User.findAndCountAll({
include: [{ model: Profile, where: { active: true } }],
limit: 3,
});
上記のクエリは、アクティブなプロフィールを持つユーザーのみをカウントします。これは、include
にwhere
句を追加すると、required
が暗黙的にtrueに設定されるためです。