預先載入
如同在關聯指南中簡要提及,預先載入是指一次查詢多個模型的資料(一個「主要」模型和一個或多個關聯模型)。在 SQL 層級,這是一個具有一個或多個聯結的查詢。
完成此操作後,Sequelize 會將關聯模型新增至傳回物件中適當命名的自動建立欄位中。
在 Sequelize 中,預先載入主要是透過在模型查找器查詢(例如 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
的新欄位。此欄位的名稱是由 Sequelize 根據關聯模型的名稱自動選擇的,其中其複數形式會在適用時使用(即,當關聯為 hasMany
或 belongsToMany
時)。換句話說,由於 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
選項,而應提供具有兩個選項的物件:model
和 as
。
請注意,使用者的 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 JOIN
的 ON
子句的條件。為了取得頂層 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;
包含所有內容
若要包含所有關聯模型,您可以使用 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 } });
包含軟刪除的記錄
如果您想要預先載入軟刪除的記錄,您可以將 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'],
],
});
在多對多關係的情況下,您也可以透過中間表中的屬性進行排序。例如,假設我們在 Division
和 Department
之間有一個多對多關係,其連接模型為 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。