对应官方文档:Model Querying - Basics
完成本章后,你应该能够:
attributes、where、order、group、limit 和 offset;Op 查询运算符,避免把用户输入拼接进 SQL;示例主要使用 Sakila 的 Actor、Film 与 World 的 City 模型。模型定义方式参见《01 Model Basics》。
本文带 await 的片段默认位于 async 函数、Express 异步路由或服务层方法中。当前工程编译为 CommonJS,不应直接使用文件顶层 await。
下面两段代码处在不同抽象层,但表达相似意图:
const actors = await Actor.findAll({
where: { lastName: "GUINESS" },
});
SELECT `actor_id`, `first_name`, `last_name`, `last_update`
FROM `actor`
WHERE `last_name` = 'GUINESS';
Sequelize 的价值是:
但索引是否命中、返回多少行、排序是否昂贵、事务是否正确,最终仍由 MySQL 执行计划决定。后端工程师不能只会 ORM API 而不看 SQL。
学习阶段建议开启:
const sequelize = new Sequelize(database, username, password, {
dialect: "mysql",
logging: console.log,
});
生产环境可以改成结构化、采样或慢查询日志,避免无差别记录敏感参数和大量成功请求。
create()const actor = await Actor.create({
firstName: "LIN",
lastName: "ZHANG",
});
console.log(actor.actorId);
create() 返回已创建的模型实例。大致 SQL:
INSERT INTO `actor` (`first_name`, `last_name`, `last_update`)
VALUES ('LIN', 'ZHANG', CURRENT_TIMESTAMP);
bulkCreate()const actors = await Actor.bulkCreate(
[
{ firstName: "ONE", lastName: "TEST" },
{ firstName: "TWO", lastName: "TEST" },
],
{
validate: true,
fields: ["firstName", "lastName"],
},
);
批量写入通常比循环执行多次 create() 更省网络往返,但要注意:
bulkCreate() 默认不会像逐条 create() 那样验证每条记录,应按需要设置 validate: true;individualHooks: true 会增加成本;MySQL 方言可使用 ignoreDuplicates: true 忽略唯一键冲突,但“忽略”可能掩盖数据质量问题,必须有明确业务语义后再用。
findAll()const actors = await Actor.findAll();
这相当于没有 WHERE 的查询,会读取整张表。Sakila 的演员表较小,但生产表可能有数百万行。接口查询通常需要条件、分页和返回列限制。
返回值是:
Actor[]
而不是单个实例。即使没有结果也返回空数组 []。
attributesconst actors = await Actor.findAll({
attributes: ["actorId", "firstName", "lastName"],
});
大致 SQL:
SELECT
`actor_id` AS `actorId`,
`first_name` AS `firstName`,
`last_name` AS `lastName`
FROM `actor` AS `Actor`;
返回列越少,通常网络传输、对象构造和内存成本越低。更重要的是,可以避免误把敏感字段带到后续响应或日志中。
const films = await Film.findAll({
attributes: {
exclude: ["description", "lastUpdate"],
},
});
exclude 在列很多、只需排除少数大字段时比较方便。但公开 API 更推荐明确列出返回字段,因为数据库以后新增列时,白名单不会意外扩大响应。
import { fn, col } from "sequelize";
const results = await Actor.findAll({
attributes: [
"lastName",
[fn("COUNT", col("actor_id")), "actorCount"],
],
group: [col("last_name")],
});
for (const result of results) {
console.log(result.lastName, result.get("actorCount"));
}
大致 SQL:
SELECT
`last_name` AS `lastName`,
COUNT(`actor_id`) AS `actorCount`
FROM `actor` AS `Actor`
GROUP BY `last_name`;
actorCount 不是 Actor 模型原本声明的持久化属性,所以通过 get("actorCount") 读取更能体现它是查询产生的别名。复杂报表查询通常更适合定义单独的结果类型或使用原始查询,而不是强行伪装成完整模型实例。
where:从对象构造查询条件const actors = await Actor.findAll({
where: {
firstName: "NICK",
lastName: "WAHLBERG",
},
});
同一对象中的字段默认使用 AND:
WHERE `first_name` = 'NICK'
AND `last_name` = 'WAHLBERG';
Opimport { Op } from "sequelize";
常用运算符:
| Sequelize | SQL 含义 |
|---|---|
Op.eq / Op.ne |
= / != |
Op.gt / Op.gte |
> / >= |
Op.lt / Op.lte |
< / <= |
Op.between / Op.notBetween |
BETWEEN / NOT BETWEEN |
Op.in / Op.notIn |
IN / NOT IN |
Op.like / Op.notLike |
LIKE / NOT LIKE |
Op.is |
IS NULL 等 |
Op.and / Op.or |
逻辑与 / 逻辑或 |
const cities = await City.findAll({
where: {
population: {
[Op.between]: [1_000_000, 5_000_000],
},
},
});
SQL 大致为:
WHERE `Population` BETWEEN 1000000 AND 5000000;
IN数组可以直接表达 IN,但显式 Op.in 更容易阅读:
const actors = await Actor.findAll({
where: {
actorId: {
[Op.in]: [1, 2, 3],
},
},
});
WHERE `actor_id` IN (1, 2, 3);
特别大的 IN 列表可能产生很长 SQL,应考虑临时表、关联表、批处理或重新设计查询来源。
LIKEconst actors = await Actor.findAll({
where: {
lastName: {
[Op.like]: "WA%",
},
},
});
WHERE `last_name` LIKE 'WA%';
这里的 % 和 _ 是 LIKE 通配符。即使 Sequelize 会把值作为参数安全处理,用户输入中的 %、_ 仍可能改变匹配语义;如果业务要求字面搜索,需要制定转义规则。
Op.iLike 是 PostgreSQL 特有运算符,不适用于 MySQL。MySQL 是否区分大小写通常取决于列的字符集和 collation,例如常见的 _ci 排序规则表示大小写不敏感。不要仅靠切换 Sequelize 运算符猜测行为。
NULLconst activeRentals = await Rental.findAll({
where: {
returnDate: {
[Op.is]: null,
},
},
});
SQL 应使用 IS NULL,而不是 = NULL:
WHERE `return_date` IS NULL;
AND、OR 和 NOT查询“姓为 GUINESS,或者名字为 PENELOPE 且主键大于 10”:
const actors = await Actor.findAll({
where: {
[Op.or]: [
{ lastName: "GUINESS" },
{
[Op.and]: [
{ firstName: "PENELOPE" },
{ actorId: { [Op.gt]: 10 } },
],
},
],
},
});
大致 SQL:
WHERE `last_name` = 'GUINESS'
OR (`first_name` = 'PENELOPE' AND `actor_id` > 10);
复杂条件最常见的问题不是语法,而是括号和业务逻辑不一致。建议:
Op.and/Op.or;如果要比较两列,可以使用 col():
import { col, Op } from "sequelize";
const rows = await SomeModel.findAll({
where: {
actualValue: {
[Op.gt]: col("expectedValue"),
},
},
});
大致 SQL:
WHERE `actual_value` > `expected_value`;
不要把列名伪装成普通字符串值,否则 Sequelize 会把它当作参数,而不是 SQL 标识符。
Model.update()const [affectedCount] = await Actor.update(
{ lastName: "UPDATED" },
{
where: {
actorId: 201,
},
},
);
console.log(`影响 ${affectedCount} 行`);
大致 SQL:
UPDATE `actor`
SET `last_name` = 'UPDATED'
WHERE `actor_id` = 201;
在 MySQL 中,不要期待通过 returning: true 像 PostgreSQL 那样直接拿到更新后的完整行。需要最新数据时再查询:
const actor = await Actor.findByPk(201);
危险代码:
await Actor.update({ lastName: "WRONG" }, { where: {} });
这可能更新整张表。实战中应:
Model.destroy()const deletedCount = await Actor.destroy({
where: {
actorId: 201,
},
});
返回值是删除行数,而不是被删除的实例:
DELETE FROM `actor` WHERE `actor_id` = 201;
清空表的选项更危险:
await TemporaryModel.destroy({
where: {},
truncate: true,
});
truncate 的事务、外键和自增值行为与普通 DELETE 不完全相同,并受 MySQL 规则影响。普通接口不应暴露这种能力。
orderconst actors = await Actor.findAll({
order: [
["lastName", "ASC"],
["firstName", "ASC"],
["actorId", "ASC"],
],
});
大致 SQL:
ORDER BY `last_name` ASC, `first_name` ASC, `actor_id` ASC;
分页查询应该有稳定排序。只按可能重复的 lastName 排序,同姓记录之间顺序不确定;追加唯一主键可以提供稳定的次序。
不要允许客户端把任意字符串直接作为排序列。应建立白名单映射:
const sortableFields = {
id: "actorId",
firstName: "firstName",
lastName: "lastName",
} as const;
type SortKey = keyof typeof sortableFields;
function getOrder(sortKey: SortKey, descending: boolean) {
const direction = descending ? "DESC" : "ASC";
return [[sortableFields[sortKey], direction]] as const;
}
如果将用户提供的列名、方向或 SQL 片段直接拼接,会带来注入风险或任意昂贵排序风险。
limit 与 offsetconst pageSize = 20;
const page = 3;
const actors = await Actor.findAll({
order: [["actorId", "ASC"]],
limit: pageSize,
offset: (page - 1) * pageSize,
});
大致 SQL:
ORDER BY `actor_id` ASC
LIMIT 20 OFFSET 40;
基础后台页面使用 offset 分页很方便,但页码越靠后,MySQL 通常仍需扫描并丢弃大量前置记录。大数据量、无限滚动或时间线接口可使用游标/键集分页:
const actors = await Actor.findAll({
where: {
actorId: { [Op.gt]: lastActorId },
},
order: [["actorId", "ASC"]],
limit: 20,
});
这依赖稳定、有索引且方向一致的游标列。多列排序时,游标条件也必须表达完整的字典序关系。
group 与聚合统计 World 中每个国家的城市数量:
import { fn, col } from "sequelize";
const counts = await City.findAll({
attributes: [
"countryCode",
[fn("COUNT", col("ID")), "cityCount"],
],
group: [col("CountryCode")],
order: [[fn("COUNT", col("ID")), "DESC"]],
});
大致 SQL:
SELECT
`CountryCode` AS `countryCode`,
COUNT(`ID`) AS `cityCount`
FROM `city` AS `City`
GROUP BY `CountryCode`
ORDER BY COUNT(`ID`) DESC;
MySQL 开启 ONLY_FULL_GROUP_BY 时,未聚合的 SELECT 列通常必须出现在 GROUP BY 中。不要为迁就错误查询而关闭严格 SQL 模式,应修正查询含义。
const actorCount = await Actor.count();
const maxPopulation = await City.max("population");
const minPopulation = await City.min("population");
const totalPopulation = await City.sum("population", {
where: { countryCode: "CHN" },
});
这些方法最终仍会执行 COUNT、MAX、MIN、SUM SQL。返回类型和空集行为应通过项目中的实际模型类型与测试确认,尤其是 DECIMAL、BIGINT 和可能返回 NULL 的聚合结果。
统计接口要关注:
WHERE 和 GROUP BY 列是否有合适索引;raw: trueconst actors = await Actor.findAll({
attributes: ["actorId", "firstName", "lastName"],
raw: true,
});
raw: true 让查询结果成为普通对象,而不是 Actor 实例。它适合只读报表、简单映射和不需要实例方法的场景。
代价包括:
save()、reload()、destroy();不要为了“性能”在整个项目全局开启 raw: true。先确认查询确实不需要模型实例,并用性能数据验证收益。
普通 where 值会由 Sequelize 处理:
await Actor.findAll({
where: { lastName: userInput },
});
这比字符串拼接安全:
// 错误示范:不要把用户输入拼成 SQL
const sql = `SELECT * FROM actor WHERE last_name = '${userInput}'`;
但参数化只保护“值”。表名、列名、排序方向等 SQL 结构不能通过普通参数替代,必须使用可信映射或白名单。
同时要区分:
ORM 主要帮助第一类,后两类仍需业务代码、索引和接口限制。
列表接口通常同时需要 where、稳定 order、limit 和精简的 attributes,任何一项都不能替代其他项。
如果接口经常执行:
WHERE CountryCode = ?
ORDER BY Population DESC, ID ASC
LIMIT 20
应结合数据分布和 EXPLAIN 评估复合索引,而不是看到 WHERE、ORDER BY 中每一列就各建一个单列索引。
LIKE '%abc' 通常难以利用普通 B-Tree 索引的前缀能力。搜索需求复杂时,应考虑全文索引或专门搜索服务,而不是无限堆叠 LIKE。
成功返回的查询也可能非常慢。可以使用 Sequelize logging 回调记录耗时,结合 MySQL 慢查询日志和 EXPLAIN 定位问题。
少量高频、读多写少的查询以后可以用 Redis 缓存普通 JSON 结果;但应先修正索引和查询,并在 MySQL 事务成功后处理缓存失效。MySQL 仍是事实来源。
where// 不推荐
await Actor.findAll({ where: req.query });
这会让客户端控制意料之外的字段和类型。应解析、校验并显式建立查询对象。
Express 的 query 参数通常来自字符串。?minPopulation=1000000 不能仅凭 TypeScript 类型断言就变成合法数字,应在运行时验证范围和整数性。
update() 返回更新实例MySQL 下静态 Model.update() 主要返回受影响行数。接口若要返回最新资源,应明确再查询,不要照搬 PostgreSQL 的 returning 经验。
没有 order,或排序列大量重复而没有唯一键兜底,会导致翻页时重复或遗漏记录。
查询对象看起来很短,并不代表 SQL 很便宜。关联、分组和子查询会进一步放大这种差异,应结合日志与 EXPLAIN 学习。
where 中的 AND、OR 和括号是否符合业务含义?raw: true 返回普通对象而非模型实例?EXPLAIN 验证重要查询?actor 表中姓以 S 开头的演员,只返回主键、姓和名,并按姓、名、主键稳定排序。actor_id 位于 50 到 100 之间,并且姓为 TEMPLE 或 ALLEN 的演员。先写目标 SQL,再写 Sequelize 查询。film 表实现列表查询:片长在 90–120 分钟、分级位于 PG 或 PG-13、按租金降序和主键升序排列,每页 20 条。rental 表查询尚未归还的租赁记录,说明为什么 return_date 条件必须使用 IS NULL 语义。city 表统计每个 CountryCode 的城市数量和总人口,按总人口降序排列。确保查询能在启用 ONLY_FULL_GROUP_BY 时正常工作。ID 为游标的分页读取 World city,比较两种方法在第 1 页和较深页码上的 SQL 与执行计划。firstName 和 lastName,要求主键有效,并检查受影响行数是否为 1。bulkCreate() 向一张自建的 Sakila 练习表批量插入 500 条记录:每 100 条一批,开启校验,并设计任意一批失败时的事务策略。name、population 或 id 排序;拒绝任意 SQL 字符串作为排序列或方向。raw: true 结果,并为聚合结果设计明确的 TypeScript DTO。