首页 / Prisma ORM 入门教程 / 原始 SQL:$queryRaw 家族与 TypedSQL

Prisma ORM 入门教程

原始 SQL:$queryRaw 家族与 TypedSQL

本教程共 54 篇 · 第 28 篇 · 更新于 2026-08-11 · 约 5 分钟阅读

原始SQL$queryRaw$executeRawSQL注入TypedSQLPrisma.sql

本节目标:学会在 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}"`);
Warning

Prisma.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 文件里,生成时自动产出带类型的函数。四个步骤:

  1. schema 的 generator 块开启 previewFeatures = ["typedSql"]
  2. 在 prisma/sql/ 目录下创建 .sql 文件(文件名必须是合法 JS 标识符)
  3. 运行 prisma generate --sql(开发时可用 --watch 监听变化)
  4. 导入生成的函数,用 $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。

Note

TypedSQL 适合静态查询;列在运行时才确定的场景只能退回 $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