
关于
TypeScript 后端的 Prisma ORM 模式——Schema 设计、查询优化、事务、分页,以及关键陷阱如 updateMany 返回计数而非记录、$transaction 超时、migrate dev 重置数据库、@updatedAt 在批量写入时被跳过和无服务器连接耗尽。
name: prisma-patterns description: TypeScript 后端的 Prisma ORM 模式——schema 设计、查询优化、事务、分页,以及关键陷阱如 updateMany 返回计数而非记录、$transaction 超时、migrate dev 重置数据库、@updatedAt 在批量写入时被跳过、无服务器连接耗尽。 origin: ECC
Prisma 模式
TypeScript 后端中 Prisma ORM 的生产模式和非显而易见的陷阱。 针对 Prisma 5.x 和 6.x 测试。某些行为与 Prisma 4 不同。
应用版本特定模式前先检查 Prisma 版本:
npx prisma --version
Prisma 5 引入了 relationJoins,可以通过 JOIN 而非单独查询来加载关系,具体取决于查询策略和配置。omit 字段修饰符和 prisma.$extends Client Extensions API 也已添加。注意:relationJoins 在大型 1:N 关系或深层嵌套 include 上可能导致行爆炸——当关系可能为每个父记录返回多行时,请对两种方法进行基准测试。
激活时机
- 设计或修改 Prisma schema 模型和关系
- 编写查询、事务或分页逻辑
- 使用
updateMany、deleteMany或任何批量操作 - 运行或规划数据库迁移
- 部署到无服务器环境(Vercel、Lambda、Cloudflare Workers)
- 实现软删除或多租户行过滤
核心概念
ID 策略
| 策略 | 适用场景 | 避免场景 |
|---|---|---|
| @default(cuid()) | 默认选择——URL 安全、可排序、无冲突 | 外部系统需要顺序 ID |
| @default(uuid()) | 需要与非 Prisma 系统互操作 | 高写入表(随机 UUID 碎片化 B-tree 索引) |
| @default(autoincrement()) | 内部关联表、审计日志 | 面向公众的 ID(暴露记录数量) |
Schema 默认值
model User {
id String @id @default(cuid())
email String @unique // @unique already creates an index — no @@index needed
name String
role Role @default(USER)
posts Post[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
@@index([createdAt])
@@index([deletedAt, createdAt]) // composite for soft-delete + sort queries
}
- 在每个外键和
WHERE或ORDER BY中使用的列上添加@@index。 - 当软删除是可预见的需求时,预先声明
deletedAt DateTime?——后续添加需要在生产表上执行迁移。 updatedAt @updatedAt仅在update和upsert时由 Prisma 自动设置(批量更新陷阱见反模式部分)。
include vs select
| | include | select |
|---|---|---|
| 返回 | 所有标量字段 + 指定关系 | 仅指定字段 |
| 适用场景 | 需要大部分字段加上关系 | 热路径、大表、避免过度获取 |
| 性能 | 宽表可能过度获取 | 最小负载,大数据集更快 |
| Prisma 5 注意 | 默认使用 JOIN(relationJoins) | 相同 |
// include — all columns + relation
const user = await prisma.user.findUnique({
where: { id },
include: { posts: { select: { id: true, title: true } } },
});
// select — explicit allowlist
const user = await prisma.user.findUnique({
where: { id },
select: { id: true, email: true, name: true },
});
永远不要从 API 响应中直接返回原始 Prisma 实体——映射到响应 DTO 以控制暴露的字段:
// BAD: leaks passwordHash, deletedAt, internal fields
return await prisma.user.findUniqueOrThrow({ where: { id } });
// GOOD: explicit DTO mapping
const user = await prisma.user.findUniqueOrThrow({ where: { id } });
return { id: user.id, name: user.name, email: user.email };
事务形式选择
| 场景 | 使用 | |---|---| | 独立操作,无相互依赖 | 数组形式 | | 后续步骤依赖前一步结果 | 交互式形式 | | 涉及外部调用(邮件、HTTP) | 完全在事务外部 |
// Array form — batched in one round trip
const [user, post] = await prisma.$transaction([
prisma.user.update({ where: { id }, data: { name } }),
prisma.post.create({ data: { title, authorId: id } }),
]);
// Interactive form — use tx client only, never the outer prisma client
const post = await prisma.$transaction(async (tx) => {
const user = await tx.user.findUniqueOrThrow({ where: { id } });
if (user.role !== 'ADMIN') throw new Error('Forbidden');
return tx.post.create({ data: { title, authorId: user.id } });
});
PrismaClient 单例
每个 PrismaClient 实例打开自己的连接池。只实例化一次。
// lib/prisma.ts
import { PrismaClient } from '@prisma/client';
const globalForPrisma = globalThis as unknown as { prisma?: PrismaClient };
export const prisma =
globalForPrisma.prisma ??
new PrismaClient({
log: process.env.NODE_ENV === 'development' ? ['query', 'error'] : ['error'],
});
if (process.env```

