预加载
正如在 关联指南 中简要提到的,预加载是指一次查询多个模型的数据(一个“主”模型和一个或多个关联模型)。在 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
的工具)的用户。这是因为当在 include
内部使用 where
选项时,Sequelize 会自动将 required
选项设置为 true
。这意味着,而不是使用 OUTER JOIN
,而是使用 INNER 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$
。因此,你可以使用它对深度嵌套的列进行复杂过滤。
为了更好地理解 include
内部使用的内部 where
选项(使用和不使用 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。