原始 SQL:$queryRaw 家族与 TypedSQL
本教程共 54 篇 · 第 28 篇 · 更新于 2026-08-11 · 约 5 分钟阅读
本节目标:学会在 Prisma Client 中安全地执行原始 SQL(即原始查询,Raw Query),掌握防注入要点,认识 TypedSQL。
什么时候用原始查询
Prisma Client 的查询 API 覆盖大部分日常场景,但有些需求表达不了:窗口函数、递归 CTE、PostGIS 地理函数、复杂报表。这时候直接把 SQL 交给数据库执行。Prisma 提供四个方法,两个安全、两个危险:
| 方法 | 返回 | 写法 | 安全性 |
|---|---|---|---|
| $queryRaw | 记录数组 | 模板标签 | 参数自动转义 |
| $executeRaw | 受影响行数 | 模板标签 | 参数自动转义 |
| $queryRawUnsafe | 记录数组 | 普通字符串 | 不转义 |
| $executeRawUnsafe | 受影响行数 | 普通字符串 | 不转义 |
$queryRaw:安全的模板查询
const email = "alice@example.com";
const users = await prisma.$queryRaw`
SELECT * FROM "User" WHERE email = ${email}
`;
$queryRaw 是标签模板(tagged template)。${email} 会被转成绑定参数交给数据库预编译,注入攻击没有可乘之机。模板变量只能放数据值,不能放表名、列名、SQL 关键字:
// 以下都不可行
await prisma.$queryRaw`SELECT * FROM ${tableName}`;
await prisma.$queryRaw`SELECT * FROM "User" ORDER BY ${order}`;
变量也不能插进 SQL 字符串字面量内部('...${x}...' 不会按预期工作)。结果默认类型是 unknown,建议显式声明:
type Row = { id: number; email: string };
const users = await prisma.$queryRaw<Row[]>`
SELECT id, email FROM "User" WHERE active = true
`;
$executeRaw:执行写入,返回行数
const count: number = await prisma.$executeRaw`
UPDATE "User" SET active = false
WHERE "lastLogin" < NOW() - INTERVAL '30 days'
`;
返回受影响的行数,不返回记录。两类方法一次只能执行一条语句,SELECT 1; SELECT 2; 这种多语句串不支持。
Unsafe 变体与 SQL 注入防护
Unsafe 方法接收普通字符串。直接把用户输入拼进 SQL,等于把门敞开:
// 危险:userInput 可能是 "' OR 1=1 --"
const rows = await prisma.$queryRawUnsafe(
`SELECT * FROM "User" WHERE email = '${userInput}'`,
);
' OR 1=1 -- 会改写整条 SQL,这就是 SQL 注入。Unsafe 方法也支持参数化写法,参数会安全转义:
const rows = await prisma.$queryRawUnsafe(
'SELECT * FROM "User" WHERE email = $1',
userInput, // PostgreSQL 用 $1,MySQL 用 ?
);
动态表名是 Unsafe 方法少有的正当用途(模板方法不支持插表名),但表名必须来自白名单:
const table = tableWhitelist[userChoice]; // 先查白名单,绝不直接取用户输入
await prisma.$queryRawUnsafe(`SELECT * FROM "${table}"`);
WarningPrisma.raw 可以把任意字符串原样塞进查询,用它等于手动关掉转义。只放完全可信的内容,绝不拼接用户输入。
Prisma.sql 组合片段
查询需要在多处拼装时,用 Prisma.sql 构造安全片段:
import { Prisma } from "./generated/prisma/client";
const ids = [1, 3, 5, 10, 20];
const users = await prisma.$queryRaw`
SELECT * FROM "User" WHERE id IN (${Prisma.join(ids)})
`;
const name = ""; // 可能为空
const result = await prisma.$queryRaw`
SELECT * FROM "User"
${name ? Prisma.sql`WHERE name = ${name}` : Prisma.empty}
`;
Prisma.join 安全地把数组拼成参数列表;Prisma.empty 表示空片段,用于条件拼接;Prisma.sql 把带参数的片段封装成对象,可以赋值、复用、传给 $queryRaw。
结果类型映射与强转
原始查询返回的值会映射成 JavaScript 类型:64 位整数变 BigInt,Decimal 变 Prisma.Decimal,DateTime 变 Date,Bytes 变 Uint8Array,Json 变 Object。BigInt 不能直接 JSON.stringify,返回给前端前记得转成字符串。
参数类型不会自动转换。PostgreSQL 的 LENGTH 只接受 text:
// 报错:function length(integer) does not exist
await prisma.$queryRaw`SELECT LENGTH(${42})`;
// 正确:显式强转
await prisma.$queryRaw`SELECT LENGTH(${42}::text)`;
TypedSQL:SQL 文件生成类型化函数
TypedSQL 是官方推荐的原始查询方案(Preview 特性):SQL 写在 .sql 文件里,生成时自动产出带类型的函数。四个步骤:
- schema 的 generator 块开启
previewFeatures = ["typedSql"] - 在 prisma/sql/ 目录下创建 .sql 文件(文件名必须是合法 JS 标识符)
- 运行
prisma generate --sql(开发时可用--watch监听变化) - 导入生成的函数,用 $queryRawTyped 执行
-- prisma/sql/getUsersByAge.sql(PostgreSQL)
-- @param {Int} $1:minAge
-- @param {Int} $2:maxAge
SELECT id, name, age FROM "User" WHERE age > $1 AND age < $2
import { getUsersByAge } from "./generated/prisma/sql";
const users = await prisma.$queryRawTyped(getUsersByAge(18, 30));
参数和返回类型都由生成器推断,写错编译期就报错。占位符随数据库不同:PostgreSQL 用 $1、MySQL 用 ?、SQLite 用 :name。生成时需要连上数据库读取表结构,先跑迁移再 generate。
NoteTypedSQL 适合静态查询;列在运行时才确定的场景只能退回 $queryRawUnsafe,注意白名单校验。MongoDB 在 v7 不支持,原始查询以 SQL 数据库为准。
ORM 与 raw 混合使用
两条路不是二选一。日常 CRUD 用 ORM,复杂报表用 raw,各取所长。两者还能在同一事务里共存:交互式事务的 tx 同样支持 $queryRaw 和 $executeRaw,一次原子提交里既有类型安全的 ORM 写入,也有手写 SQL 的统计更新。
参考来源
- Prisma 官方文档:Raw queries / TypedSQL / Write your own SQL
- Mapagam:Working with Raw SQL Queries
- DevSheets:Raw SQL Queries