24 fn、col、literal:值、列与 SQL 表达式
这三者不是“更高级的查询参数”,而是帮助你表达 SQL 中不同种类的内容。对应项目:客户统计。
import { Op, fn, col, literal, where as sqlWhere } from "sequelize";
1. 三种内容,不要混成一个字符串
| 写法 | 表示什么? | SQL 思维 |
|---|---|---|
普通值 "first_name" |
在函数参数等值位置,是文本值 | 'first_name' |
col("first_name") |
SQL 列引用 | `first_name` |
fn("UPPER", col("first_name")) |
函数调用 | UPPER(first_name) |
literal("固定 SQL") |
原样 SQL 片段 | 不再自动转义片段 |
一个细节:在 attributes: ["first_name"]、order: [["first_name", "ASC"]] 这些 API 特定位置,普通字符串本来就被解释为列。不要把“字符串在任何地方都是值”当成规律;看它所在的参数位置。
2. col 是数据库列,不是 JS 对象取值
const rows = await Customer.findAll({
attributes: ["customer_id", [col("first_name"), "given_name"]],
raw: true,
});
SELECT customer_id, first_name AS given_name FROM customer;
col() 不会读取某个 customer 实例,也不会因为写了 col("address.city.country_id") 就自动 JOIN 三张表。关联还需要 include,或 Raw SQL 中显式 JOIN。
多表有同名字段时需要限定列所属表/别名。SQL 表别名、关联名和返回对象路径有关联,但不是随处都能互换。
本项目版本已核对下面的写法:
const rows = await Customer.findAll({
attributes: [
"customer_id",
[col("address.city.country_id"), "country_id"],
],
include: [{
model: Address, as: "address", attributes: [],
include: [{ model: City, as: "city", attributes: [] }],
}],
raw: true,
});
实际生成的 SQL 核心是:
SELECT `Customer`.`customer_id`,
`address->city`.`country_id` AS `country_id`
FROM `customer` AS `Customer`
LEFT OUTER JOIN `address` AS `address`
ON `Customer`.`address_id` = `address`.`address_id`
LEFT OUTER JOIN `city` AS `address->city`
ON `address`.`city_id` = `address->city`.`city_id`;
v6 的 col 点号路径在这里被处理成嵌套 JOIN 别名。更换 include、分页子查询、别名压缩或版本后必须重新核对,不能推导所有路径都合法。
另一个易错点:如果 Model 用 field 把 TS 属性映射到不同的数据库列名,col() 不应被当成“必定自动完成 field 映射”的工具。当前工程统一 snake_case,减少了这一层差异。
3. fn:函数名 + 参数
拼完整姓名
attributes: [[
fn("CONCAT", col("first_name"), " ", col("last_name")),
"full_name",
]]
SELECT CONCAT(first_name, ' ', last_name) AS full_name FROM customer;
中间的空格是文本值;姓名列要用 col。
错误对照:
fn("UPPER", "first_name")
生成 UPPER('first_name'),得到的是常量 FIRST_NAME,而不是每个客户姓名的大写。
统计与嵌套函数
fn("COUNT", col("customer_id"))
fn("COALESCE", fn("SUM", col("amount")), 0)
COUNT(customer_id)
COALESCE(SUM(amount), 0)
第二个表达式适合对 payment 表做聚合;在 Customer 表直接写 SUM(amount) 会报未知列。fn 不检查表是否有该列,也不替你建立 Payment Model。
函数名也应由开发者固定,不能接收任意用户输入。可生成 SQL 不代表目标数据库有同名函数:MySQL 与 PostgreSQL 函数并非完全相同。
4. where():表达式左边不一定是普通字段
这里是 Sequelize 导出的 where() 函数,不是 options 中的 where 对象。使用别名 sqlWhere 避免混淆。
需求:按姓名小写值匹配。
const rows = await Customer.findAll({
where: sqlWhere(fn("LOWER", col("first_name")), Op.eq, "ann"),
});
SELECT ... FROM customer WHERE LOWER(first_name) = 'ann';
数据库排序规则可能本来就不区分大小写;对列套函数还可能影响普通索引使用。学会表达式,不意味着每次搜索都应该 LOWER。
列与列比较:
where: sqlWhere(col("first_name"), Op.eq, col("last_name"))
WHERE first_name = last_name
而 { first_name: "last_name" } 表示 first_name = 'last_name'。这些列比较仍然受 MySQL 排序规则的比较语义影响。
表达式与普通条件可以组合:
where: {
[Op.and]: [
{ active: 1 },
sqlWhere(fn("CHAR_LENGTH", col("first_name")), Op.gt, 3),
],
}
WHERE active = 1 AND CHAR_LENGTH(first_name) > 3
5. literal:由你负责整个片段
固定 CASE 标签适合演示 literal:
const rows = await Customer.findAll({
attributes: [
"customer_id",
[literal("CASE WHEN `active` = 1 THEN 'enabled' ELSE 'disabled' END"), "status_label"],
],
raw: true,
});
SELECT customer_id,
CASE WHEN active = 1 THEN 'enabled' ELSE 'disabled' END AS status_label
FROM customer;
literal 不自动转义这个 CASE 内的 SQL。上例之所以可接受,是片段完全固定、没有用户输入。这里查询只有一张表;多表场景要限定 active 所属别名。
再看与当前客户详情类似的固定标量子查询:
attributes: [
"customer_id",
[literal(`(
SELECT COALESCE(SUM(p.amount), 0)
FROM payment AS p
WHERE p.customer_id = \`Customer\`.\`customer_id\`
)`), "payment_total"],
]
它关联外层 Customer.customer_id,每个客户获得一个总额。依赖外层 SQL 别名,写错会报错;还需要 EXPLAIN 和数据量评估。复杂详情查询采用独立 Raw SQL 往往更清楚。
6. 最危险的误用:literal 插值用户输入
// 危险示例,禁止这样写
literal(`first_name = '${input_name}'`)
用户输入变成 SQL 语法的一部分。即使 Controller 做过某些长度校验,也没有解决注入。
选择规则:
- 简单值条件:
where: { first_name: input_name }。 - 复杂且需要参数:完整 Raw SQL +
replacements或bind。 - 列名、排序方向、函数名:固定代码或白名单,不是任意字符串绑定。
- literal:尽量只放固定片段。需要值占位符时必须核对当前 API/版本是否处理,不能假设所有嵌套 literal 都自动安全。
const rows = await db.query<{ payment_total: string }>(
"SELECT COALESCE(SUM(amount), 0) AS payment_total FROM payment WHERE customer_id = :customer_id",
{ type: QueryTypes.SELECT, replacements: { customer_id } },
);
replacements 由 Sequelize 转义并替换;bind 将值传给驱动绑定。两者只处理值,不把用户输入安全地变成表名、列名或 ORDER 方向。
7. 不要为了使用 fn 把简单问题复杂化
只要数量:Rental.count({ where: { customer_id } }) 更直接。
查询多个独立统计、跨未定义的 Model、复杂子查询:Raw SQL 更易评审。
普通 CRUD、简单关联:Model API 类型与实例方法更方便。
选择的目标是可读、可验证、安全,而不是“所有 SQL 都要拼成 ORM 表达式”。计算列通过实例 get() 读取,或用独立查询行类型,不自动变成 Model 固有属性。
8. 练习
- 比较
fn("UPPER", "first_name")与fn("UPPER", col("first_name"))的 SQL 和结果。 - 使用 sqlWhere 比较 first_name 与 last_name。
- 去掉 City include,解释 country_id 的 col 为什么失败。
- 将固定 CASE literal 改为 DTO 中的 boolean 转文字,比较两层实现的取舍。
- 审查所有 literal:是否含用户输入?是否依赖未经核对的别名?