本文基于 Sequelize v6 官方文档,示例使用 TypeScript、MySQL 与
mysql2。这一章关注“怎样得到查询结果”;复杂的where、排序、分页和运算符应结合上一篇 Querying Basics 学习。
完成本章后,你应该能够:
findAll、findOne 与 findByPk;findOrCreate、findAndCountAll 和聚合查找器;以下模型映射 Sakila 的 actor 表。Sakila 表名、字段名采用下划线风格,而且 actor 不使用 Sequelize 默认的 createdAt、updatedAt 字段,因此需要显式配置。
import {
CreationOptional,
DataTypes,
InferAttributes,
InferCreationAttributes,
Model,
Sequelize,
} from "sequelize";
const sequelize = new Sequelize("sakila", "root", "password", {
host: "127.0.0.1",
dialect: "mysql",
logging: console.log, // 学习阶段建议观察 SQL
});
class Actor extends Model<
InferAttributes<Actor>,
InferCreationAttributes<Actor>
> {
declare actorId: CreationOptional<number>;
declare firstName: string;
declare lastName: string;
declare lastUpdate: CreationOptional<Date>;
}
Actor.init(
{
actorId: {
type: DataTypes.SMALLINT.UNSIGNED,
autoIncrement: true,
primaryKey: true,
field: "actor_id",
},
firstName: {
type: DataTypes.STRING(45),
allowNull: false,
field: "first_name",
},
lastName: {
type: DataTypes.STRING(45),
allowNull: false,
field: "last_name",
},
lastUpdate: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW,
field: "last_update",
},
},
{
sequelize,
tableName: "actor",
timestamps: false,
},
);
field负责把 TypeScript 中的actorId映射为 MySQL 中的actor_id。业务代码可以使用一致的 camelCase,而不必改变现有数据库结构。
大多数查找器返回 Actor 实例。实例既包含字段数据,也包含 save()、update()、destroy() 等方法。
const actor = await Actor.findByPk(1);
if (actor !== null) {
console.log(actor.firstName);
console.log(actor.get({ plain: true }));
}
如果只需要序列化结果,可以调用:
const plainActor = actor?.get({ plain: true });
const jsonActor = actor?.toJSON();
也可以在查询中使用 raw: true 直接得到普通对象:
const actors = await Actor.findAll({
attributes: ["actorId", "firstName", "lastName"],
raw: true,
});
raw: true 适合只读列表、统计和导出,但结果没有实例方法,关联查询的结构也可能变得扁平。不要为了“看起来轻量”就在整个项目中全局使用它。
findAll:查询零到多行const actors = await Actor.findAll({
attributes: ["actorId", "firstName", "lastName"],
order: [["actorId", "ASC"]],
limit: 20,
offset: 0,
});
结果类型是 Actor[]。没有记录时返回空数组 [],不会返回 null。
近似 SQL:
SELECT actor_id AS actorId, first_name AS firstName, last_name AS lastName
FROM actor
ORDER BY actor_id ASC
LIMIT 0, 20;
工程建议:列表接口只选择实际需要的列。SELECT * 会增加网络传输、对象构造与序列化成本,还容易在表新增敏感字段后意外把它返回给客户端。
findByPk:按主键查一行const actor = await Actor.findByPk(42, {
attributes: ["actorId", "firstName", "lastName"],
});
if (actor === null) {
// 在 HTTP 接口中通常转换为 404
}
近似 SQL:
SELECT actor_id, first_name, last_name
FROM actor
WHERE actor_id = 42
LIMIT 1;
findByPk 表达了明确意图,也会使用模型声明的主键;不要把它误解成“默认查 id 字段”。如果模型主键映射错误,查询也会错误。
findOne:按条件取第一行const actor = await Actor.findOne({
where: { lastName: "GUINESS" },
order: [["actorId", "ASC"]],
});
结果是 Actor | null。findOne 通常生成带 LIMIT 1 的 SQL。
这里的“第一行”只有在指定 order 时才有稳定业务含义。若数据库中可能存在多个 last_name = 'GUINESS' 的演员,又没有排序,那么不要假设每次都会返回同一行。
如果业务上要求条件唯一,例如使用邮箱查询用户,应该在 MySQL 中建立 UNIQUE 约束,而不是依赖 findOne 把重复数据隐藏起来。
rejectOnEmpty:把空结果转成异常默认情况下,findOne 与 findByPk 查不到时返回 null。设置 rejectOnEmpty: true 后会抛出 SequelizeEmptyResultError:
const actor = await Actor.findByPk(42, {
rejectOnEmpty: true,
});
也可以传入自定义错误实例:
class ActorNotFoundError extends Error {}
const actor = await Actor.findByPk(42, {
rejectOnEmpty: new ActorNotFoundError("演员不存在"),
});
是否使用它取决于团队的错误处理风格:
null 往往更直观;findOrCreate:找到或创建const [actor, created] = await Actor.findOrCreate({
where: {
firstName: "ALICE",
lastName: "ZHANG",
},
defaults: {
lastUpdate: new Date(),
},
});
console.log(actor.actorId);
console.log(created); // true 表示本次创建;false 表示查到已有记录
返回的是二元组 [instance, created]。where 是查找条件;只有需要创建时,defaults 才参与新记录构造。如果同一字段同时出现在二者中,创建时通常以 where 中的值为准。
两个请求可能同时判断记录不存在,然后同时插入。要保证业务唯一性,必须建立 MySQL 唯一约束。例如若业务定义姓名组合唯一(Sakila 原表并没有这个约束),应由数据库约束最终兜底。
findOrCreate 在内部会使用事务来处理常见竞争情况,但它不能替代正确的数据模型。生产实践是:
findOrCreate 表达意图;UniqueConstraintError 等竞争结果。findAndCountAll:分页数据与总数const page = 2;
const pageSize = 20;
const result = await Actor.findAndCountAll({
where: { lastName: "GUINESS" },
order: [["actorId", "ASC"]],
limit: pageSize,
offset: (page - 1) * pageSize,
});
console.log(result.count);
console.log(result.rows);
它通常执行两条 SQL:一条 COUNT,一条查询当前页。不要把它理解为“一次数据库访问”。
SELECT count(*) AS count FROM actor WHERE last_name = 'GUINESS';
SELECT ... FROM actor
WHERE last_name = 'GUINESS'
ORDER BY actor_id ASC
LIMIT 20, 20;
使用关联 include 后,需要重点检查计数是否因一对多 JOIN 而重复。常见做法是根据查询结构评估 distinct: true,并观察实际 SQL:
const result = await Actor.findAndCountAll({
distinct: true,
limit: 20,
offset: 0,
// include: [...]
});
另外,只有 include 中设置了 required: true 的关联会参与计数条件。复杂报表中,分开写明确的 count() 与 findAll() 往往更容易控制和优化。
OFFSET 200000 仍可能要求 MySQL 扫描并丢弃大量记录。面向不断向后翻页的列表,可以考虑基于稳定索引的游标分页:
import { Op } from "sequelize";
const actors = await Actor.findAll({
where: {
actorId: { [Op.gt]: 2000 },
},
order: [["actorId", "ASC"]],
limit: 20,
});
findAndCountAll 中 count 的类型变化没有 group 时,count 通常是一个数字。加入 group 后,count 会变成分组计数结果数组,而不是单个总数:
const result = await Actor.findAndCountAll({
attributes: ["lastName"],
group: ["lastName"],
});
console.log(result.count); // 分组计数数组
因此通用分页函数不能在完全不看查询参数的情况下假设 count 永远是 number。
count、max、min 与 sumSequelize 提供常用聚合查找器:
const actorCount = await Actor.count();
const largestActorId = await Actor.max("actorId");
const smallestActorId = await Actor.min("actorId");
const actorIdSum = await Actor.sum("actorId");
它们都可以接收 where:
const guinessCount = await Actor.count({
where: { lastName: "GUINESS" },
});
近似 SQL:
SELECT count(*) AS count FROM actor WHERE last_name = 'GUINESS';
SELECT max(actor_id) AS max FROM actor;
注意 MySQL DECIMAL 的精度策略。金额聚合结果在不同配置和驱动选项下可能以字符串表示;不要为了方便直接用 JavaScript number 做高精度金额计算。
路由层负责 HTTP,service 层负责业务规则,可以让“没有找到”的含义更明确:
export async function getActorById(actorId: number): Promise<Actor> {
const actor = await Actor.findByPk(actorId);
if (actor === null) {
throw new ActorNotFoundError(`演员 ${actorId} 不存在`);
}
return actor;
}
不要从 controller 把整个 req.query 原样传给 Sequelize。应先校验和转换允许的分页、排序、筛选字段,否则容易产生不可控查询,甚至引入 SQL 注入风险。Sequelize 参数化查询能保护“值”,但动态列名和排序字段仍应使用白名单。
logging,理解 ORM 生成的 SQL;attributes;findAll 返回数组;findOne、findByPk 返回实例或 null;raw: true 返回普通结果,但失去实例能力;findOrCreate 方便表达“有则取、无则建”,数据库唯一约束仍不可缺少;findAndCountAll 通常执行计数和列表两条查询;actor 表定义 TypeScript 模型,查询 actor_id = 10 的演员;查不到时抛出自定义错误。findAll 查询姓氏为 DAVIS 的演员,只返回主键、名和姓,并按主键降序排列。findOne 查询 World 数据库 city 表中人口最多的城市,并解释为什么必须设置 order。findAndCountAll 查询,返回总数、页码、每页数量和列表。actor_id 的游标分页,比较它与大 offset 分页的适用场景。country 表分别统计国家总数、最大人口和最小非零人口,观察 Sequelize 生成的 SQL。findOrCreate 创建 World city 记录的示例,并说明要防止并发重复,数据库应建立什么唯一约束。raw: true 查询 5 个演员,比较返回结构、可用方法和适用场景。