跳到主要內容
版本:v6 - 穩定版

預先載入

如同在關聯指南中簡要提及,預先載入是指一次查詢多個模型的資料(一個「主要」模型和一個或多個關聯模型)。在 SQL 層級,這是一個具有一個或多個聯結的查詢。

完成此操作後,Sequelize 會將關聯模型新增至傳回物件中適當命名的自動建立欄位中。

在 Sequelize 中,預先載入主要是透過在模型查找器查詢(例如 findOnefindAll 等)上使用 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 Usertrue。這表示當 Sequelize 擷取關聯模型時,它們會以模型實例的形式新增至輸出物件中。

在上方,關聯模型已新增至擷取的任務中名為 user 的新欄位。此欄位的名稱是由 Sequelize 根據關聯模型的名稱自動選擇的,其中其複數形式會在適用時使用(即,當關聯為 hasManybelongsToMany 時)。換句話說,由於 Task.belongsTo(User),任務會關聯至一個使用者,因此邏輯選擇是單數形式(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
}
]
}
]

請注意,存取器(結果實例中的 tasks 屬性)會複數化,因為關聯是一對多。

擷取具有別名的關聯

如果關聯具有別名(使用 as 選項),您必須在包含模型時指定此別名。您不應將模型直接傳遞給 include 選項,而應提供具有兩個選項的物件:modelas

請注意,使用者的 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

必要的預先載入

當預先載入時,我們可以強制查詢僅傳回具有關聯模型的記錄,有效地將查詢從預設的 OUTER JOIN 轉換為 INNER JOIN。這是透過 required: true 選項完成,如下所示

User.findAll({
include: {
model: Task,
required: true,
},
});

此選項也適用於巢狀包含。

在關聯模型層級篩選的預先載入

當預先載入時,我們也可以使用 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 查詢將僅擷取至少有一個工具符合條件(在本例中為非 small)的使用者。由於當 where 選項在 include 內使用時,Sequelize 會自動將 required 選項設定為 true,因此會發生這種情況。這表示,將執行 INNER JOIN,而不是 OUTER JOIN,並且僅傳回具有至少一個相符子系的父模型。

另請注意,使用的 where 選項已轉換為 INNER JOINON 子句的條件。為了取得頂層 WHERE 子句,而不是 ON 子句,必須執行不同的操作。接下來將顯示此操作。

參考其他欄位

如果您想要在包含的模型中套用參考關聯模型值的 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$' 語法。

例如,它可以用來將包含的模型中的 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$。因此,您可以使用此語法在深度巢狀欄位上建立複雜的篩選器。

為了更好地理解內部 where 選項(在 include 內使用)的所有差異,以及是否使用 required 選項,以及使用 $nested.column$ 語法的頂層 where,以下為您提供四個範例

// 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 載入的 - 也就是說,它只包含父資料表中的記錄。如果您使用的方言支援,您可以透過傳遞 right 選項將此行為變更為 RIGHT OUTER JOIN

目前,SQLite 不支援右聯結

注意:只有當 required 為 false 時,才會遵守 right

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
}]
});

多重預先載入

include 選項可以接收陣列,以便一次擷取多個關聯模型

Foo.findAll({
include: [
{
model: Bar,
required: true
},
{
model: Baz,
where: /* ... */
},
Qux // Shorthand syntax for { model: Qux } also works here
]
})

使用多對多關係進行預先載入

當您在具有 Belongs-to-Many 關係的模型上執行預先載入時,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
}
}
]
}

請注意,每個預先載入到 "Bars" 屬性中的 bar 實例都有一個名為 Foo_Bar 的額外屬性,這是聯結模型的相關 Sequelize 實例。預設情況下,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 的 through 選項內套用 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;

包含所有內容

若要包含所有關聯模型,您可以使用 allnested 選項

// 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 } });

包含軟刪除的記錄

如果您想要預先載入軟刪除的記錄,您可以將 include.paranoid 設定為 false 來執行此操作

User.findAll({
include: [
{
model: Tool,
as: 'Instruments',
where: { size: { [Op.ne]: 'small' } },
paranoid: false,
},
],
});

為預先載入的關聯排序

當您想要將 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'],
],
});

在多對多關係的情況下,您也可以透過中間表中的屬性進行排序。例如,假設我們在 DivisionDepartment 之間有一個多對多關係,其連接模型為 DepartmentDivision,您可以這樣做:

Company.findAll({
include: {
model: Division,
include: Department,
},
order: [[Division, DepartmentDivision, 'name', 'ASC']],
});

在以上所有範例中,您都注意到 order 選項是用在頂層。 order 也可在 include 選項內運作的唯一情況是使用 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']],
},
});

包含子查詢的複雜排序

請參考關於子查詢的指南,了解如何使用子查詢來輔助更複雜的排序。

巢狀預先載入

您可以使用巢狀預先載入來載入相關模型的所有相關模型。

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 相關聯的教師。

findAndCountAll 與 includes 一起使用

findAndCountAll 實用函式支援 includes。只有標記為 required 的 includes 會被納入 count。例如,如果您想尋找並計算所有有配置文件的使用者:

User.findAndCountAll({
include: [{ model: Profile, required: true }],
limit: 3,
});

由於 Profile 的 include 設定了 required,它將產生一個內部聯結,並且只會計算有配置文件的使用者。如果我們從 include 中移除 required,則無論有無配置文件的使用者都會被計算。在 include 中新增 where 子句會使其自動變為 required。

User.findAndCountAll({
include: [{ model: Profile, where: { active: true } }],
limit: 3,
});

上面的查詢只會計算有活躍配置文件的使用者,因為當您在 include 中新增 where 子句時,required 會被隱式地設定為 true。