Chapter 07

事务与原始查询

用 $transaction 保证原子性、用交互式事务处理条件逻辑、用 $queryRaw 安全执行原始 SQL,并掌握乐观并发。

7.1 核心概念

事务(transaction)
一组要么全成、要么全败的数据库操作,保证 ACID。Prisma 提供两种:批处理数组式与交互式回调式。
批处理事务(sequential $transaction)
传入 Promise 数组,Prisma 顺序执行并包在同一事务里,任一失败全部回滚。适合无条件依赖的多写入。
交互式事务(interactive transaction)
传入 async (tx) => {} 回调,在回调里用 tx 做查询并按结果分支决策。抛错则回滚。
隔离级别(isolation level)
控制并发事务可见性:ReadCommitted、RepeatableRead、Serializable 等,隔离越强并发越低、越安全。
$queryRaw / $queryRawUnsafe
执行返回结果的原始 SQL。$queryRaw 用标签模板参数化(防注入),Unsafe 变体拼字符串(需自行防注入)。
$executeRaw
执行不返回行、只返回受影响行数的原始 SQL(INSERT/UPDATE/DELETE/DDL)。
乐观并发(optimistic concurrency)
不加锁,靠版本号/时间戳字段在更新时校验;若期间被他人改动则更新命中 0 行,据此重试或报冲突。

7.2 批处理事务

// 转账:两条更新要么都成功要么都回滚
const [from, to] = await prisma.$transaction([
  prisma.account.update({ where: { id: 1 }, data: { balance: { decrement: 100 } } }),
  prisma.account.update({ where: { id: 2 }, data: { balance: { increment: 100 } } }),
]);

7.3 交互式事务

await prisma.$transaction(async (tx) => {
  const acc = await tx.account.findUnique({ where: { id: 1 } });
  if (!acc || acc.balance < 100) {
    throw new Error("余额不足");   // 抛错 → 整个事务回滚
  }
  await tx.account.update({ where: { id: 1 }, data: { balance: { decrement: 100 } } });
  await tx.account.update({ where: { id: 2 }, data: { balance: { increment: 100 } } });
}, {
  maxWait: 5000,           // 等待连接的最长毫秒
  timeout: 10000,          // 事务最长执行毫秒
  isolationLevel: Prisma.TransactionIsolationLevel.Serializable,
});
交互式事务要短

事务期间会占用连接并可能持锁。回调里别做耗时的网络/文件 IO,也别把无关逻辑塞进去,否则拖长事务、加剧锁竞争与连接耗尽。

7.4 原始 SQL:$queryRaw

import { Prisma } from "@prisma/client";

// 参数化标签模板——自动转义,防 SQL 注入
const email = "bob@example.com";
const users = await prisma.$queryRaw<Array<{ id: number; email: string }>>`
  SELECT id, email FROM "User" WHERE email = ${email}
`;

// 动态拼片段用 Prisma.sql / Prisma.join,仍保持参数化
const ids = [1, 2, 3];
const rows = await prisma.$queryRaw(
  Prisma.sql`SELECT * FROM "Post" WHERE "authorId" IN (${Prisma.join(ids)})`
);
切勿字符串拼接 SQL

$queryRawUnsafe(`... ${userInput}`) 直接拼接用户输入 = SQL 注入。必须用标签模板 $queryRaw`...${v}`Prisma.sql,让 Prisma 做参数绑定。

7.5 $executeRaw

// 返回受影响行数
const affected = await prisma.$executeRaw`
  UPDATE "Post" SET "views" = "views" + 1 WHERE "id" = ${postId}
`;

7.6 乐观并发控制

// schema 加 version 字段;更新时以旧 version 为条件
const res = await prisma.post.updateMany({
  where: { id: postId, version: currentVersion },
  data: { title: newTitle, version: { increment: 1 } },
});
if (res.count === 0) {
  throw new Error("记录已被他人修改,请刷新重试");
}

7.7 批量操作对比

方法用途是否原子
createMany批量插入单条 SQL,是
updateMany / deleteMany按条件批量改删单条 SQL,是
$transaction([...])多个不同操作打包
循环里逐条 update不推荐否(除非包事务)

7.8 小结与下一步