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 做过某些长度校验,也没有解决注入。

选择规则:

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. 练习

  1. 比较 fn("UPPER", "first_name") 与 fn("UPPER", col("first_name")) 的 SQL 和结果。
  2. 使用 sqlWhere 比较 first_name 与 last_name。
  3. 去掉 City include,解释 country_id 的 col 为什么失败。
  4. 将固定 CASE literal 改为 DTO 中的 boolean 转文字,比较两层实现的取舍。
  5. 审查所有 literal:是否含用户输入?是否依赖未经核对的别名?

下一章:GROUP、HAVING 与聚合。参考:v6 Model Querying Basics。