22 include 参数实战:连接数据与筛选主体
对应项目:customerAddressInclude()、listActorFilms()、关联注册。
1. 先确定“这条列表的一项是什么”
客户列表的一项是 Customer,适合 Customer.findAll()。演员的电影列表可以以 FilmActor 为主体,每一项表示一个演员与电影的关系。
不要先问“能不能把所有表 JOIN 到一起”。先写出主体、关联基数和返回形状:
Customer → Address → City → Country:每一段都是 belongsTo,最多一个对象
Customer → Rental:hasMany,一个客户可以有多条租赁
Actor ↔ Film:belongsToMany,经过 FilmActor
FilmActor → Film:belongsTo,每条关系对应一部电影
前一条链通常不放大客户行;多值关联可能放大。这个区别会影响计数、分页和聚合。
2. model、as 与 association
const customers = await Customer.findAll({
attributes: ["customer_id", "first_name"],
include: [{
model: Address,
as: "address",
attributes: ["address_id", "address"],
}],
limit: 10,
});
等价 SQL:
SELECT c.customer_id, c.first_name, a.address_id, a.address
FROM customer AS c
LEFT JOIN address AS a ON c.address_id = a.address_id
LIMIT 10;
默认得到实例,关联挂在 customer.address。as 必须与注册时的名字一致,不是任意 SQL 别名。
也可以明确引用已注册关联:
include: [{
association: Customer.associations.address!,
attributes: ["address_id", "address"],
}]
这里非空断言依赖“关联已经初始化”的前提,并不验证初始化成功。初学阶段使用 model + as 更直观;字符串 association: "address" 也可用,但拼错仍是运行时错误。
include 不会自动创建模型、外键或实际表。它使用已经定义的 Association 组织查询。
3. required 决定是否排除主体记录
查询所有客户,只附带未归还租赁
include: [{
model: Rental,
as: "rentals",
attributes: ["rental_id", "return_date"],
where: { return_date: null },
required: false,
}]
等价 SQL:
SELECT c.customer_id, r.rental_id, r.return_date
FROM customer AS c
LEFT JOIN rental AS r
ON r.customer_id = c.customer_id AND r.return_date IS NULL;
没有未归还租赁的客户仍返回,通常 rentals: []。条件限制的是附带的数据。
只查询有未归还租赁的客户
把 required 改成 true:
SELECT c.customer_id, r.rental_id, r.return_date
FROM customer AS c
INNER JOIN rental AS r
ON r.customer_id = c.customer_id AND r.return_date IS NULL;
没有匹配租赁的客户被排除。include.where 存在且没有显式 required 时,本版本通常默认 required: true。因此“只加一个子表 where”可能改变父表列表含义。
4. include.where 与顶层 where 的差别
上面的 required: false + include.where 是“保留客户,但只加载匹配租赁”。
顶层路径条件则放到整个查询的 WHERE:
const customers = await Customer.findAll({
where: { "$rentals.rental_id$": { [Op.ne]: null } },
include: [{ model: Rental, as: "rentals", required: false }],
});
SELECT ... FROM customer AS c
LEFT JOIN rental AS r ON r.customer_id = c.customer_id
WHERE r.rental_id IS NOT NULL;
客户没有租赁时,连接产生的 NULL 被 WHERE 排除。虽然 JOIN 写的是 LEFT,最终结果已不保留这些客户。
特别易错的是顶层 return_date IS NULL:LEFT JOIN 没有租赁也会生成 NULL,可能把“完全没有租赁”的客户算进去。要区分“有未归还租赁”和“没有关联”。
顶层 $关联.字段$ 在开启分页子查询后还可能遇到别名作用域问题。不是所有 JOIN 条件都应移动到顶层;先看实际 SQL。
5. Customer 国家筛选:沿途都要参与筛选
国家外键 country_id 在 City,不在 Customer。
include: [{
model: Address,
as: "address",
required: true,
include: [{
model: City,
as: "city",
required: true,
where: { country_id },
include: [{ model: Country, as: "country", required: true }],
}],
}]
等价 SQL:
SELECT ...
FROM customer AS c
INNER JOIN address AS a ON a.address_id = c.address_id
INNER JOIN city AS ci ON ci.city_id = a.city_id AND ci.country_id = :country_id
INNER JOIN country AS co ON co.country_id = ci.country_id;
如果最外层 Address 仍是 LEFT,即使内部 City 是 INNER,外层仍可能保留没有匹配路径的客户,只是地址为 null。最深层 required 不自动把所有父层变成 INNER。
当前 include 工厂:没有国家筛选时加载资料;有筛选时沿途 required=true。工厂复用的是完整查询意图,不只是省几个字符。
6. belongsToMany 的 through
const actor = await Actor.findByPk(actor_id, {
attributes: ["actor_id", "first_name", "last_name"],
include: [{
model: Film,
as: "films",
attributes: ["film_id", "title"],
through: { attributes: [] },
required: false,
}],
});
等价 SQL 要经过中间表:
SELECT a.actor_id, a.first_name, a.last_name, f.film_id, f.title
FROM actor AS a
LEFT JOIN film_actor AS fa ON fa.actor_id = a.actor_id
LEFT JOIN film AS f ON f.film_id = fa.film_id
WHERE a.actor_id = :actor_id;
through.attributes: [] 只是不给结果附带中间表字段,不是跳过中间表 JOIN。
若中间表有需要的字段,例如当前 last_update:
through: {
attributes: ["last_update"],
where: { last_update: { [Op.gte]: start_at } },
}
条件作用于 film_actor,不是 film。是否保留没有匹配关系的演员仍需结合 required 和实际 SQL 判断。
当前电影列表直接以 FilmActor 为基表:
const rows = await FilmActor.findAll({
where: { actor_id },
include: [{ model: Film, as: "film", required: true }],
order: [
[{ model: Film, as: "film" }, "title", "ASC"],
["film_id", "ASC"],
],
limit: 20,
});
固定 actor_id 时,复合主键保证每个 film_id 只有一条关系;include Film 是单值,因此一条关系对应一条列表项,分页更容易理解。
7. 关联排序放在哪里?
普通 JOIN 加载时,关联排序通常写在顶层:
order: [[{ model: Film, as: "films" }, "title", "ASC"]]
多层关联就按关联路径提供多个 model/as 项。不要把普通 JOIN 的 order 随意放进 include,以为每个子数组一定按它排序。
separate: true 的 hasMany include 则支持在 include 内设置 order。也要区分“按电影标题排列关联数组”和“按电影某个聚合值排列演员”,后者通常需要单独定义聚合规则。
8. separate 与 include.limit:不是所有关联都支持
需求:取 20 个客户,每个客户最多附带最近 3 条租赁。
const customers = await Customer.findAll({
attributes: ["customer_id", "first_name"],
order: [["customer_id", "ASC"]],
limit: 20,
include: [{
model: Rental,
as: "rentals",
attributes: ["rental_id", "customer_id", "rental_date"],
separate: true,
limit: 3,
order: [["rental_date", "DESC"], ["rental_id", "DESC"]],
}],
});
先查父记录,再查关联记录。关联查询可能使用批量条件或分组限制的 UNION 等 SQL,不承诺“永远只有两条简单查询”。
Sequelize v6 当前实现只允许 hasMany 使用 separate。不能把它直接加到 Actor.belongsToMany(Film)。include.limit 在未显式设置 separate 时可能触发 separate,从而报 Only HasMany associations support include.separate。
separate 子查询的 where 限制子数据,不应当拿它当作父表“必须有匹配孩子”的过滤方案;父表筛选可另用 EXISTS 或必要的 JOIN。
9. subQuery 与 duplicating:先别拿来救火
subQuery用于控制父表分页子查询;多值 JOIN 下关闭它,LIMIT 可能限制 JOIN 行而非父记录数。duplicating: false表示告诉 ORM 该关联不会放大主体行;它不是去重操作。distinct: true主要修正 COUNT 语义,不能保证所有分页与关联装配都正确。
如果租赁真的一对多,强行标记 duplicating=false 是错误承诺。先选分页主体,再考虑“父表分页后批量加载关联”、separate 或专门 SQL。详见 23 章。
10. 练习
- 比较 Rental where + required=false / true,解释没有租赁的客户去哪里了。
- 国家筛选只改最深层 required,再与沿途 required=true 对照。
- 取两个客户及所有租赁,观察 SQL 行数与实例数组长度为何不同。
- 在 FilmActor 基表查询中增加电影标题过滤,确认过滤列的所属表。
下一章:排序、分页与计数。参考:v6 Eager Loading。