软删除模式与生产 Schema 设计
本教程共 54 篇 · 第 45 篇 · 更新于 2026-08-11 · 约 5 分钟阅读
本节目标:掌握软删除的完整生命周期(标记、过滤、恢复、硬删、归档),并学会设计生产级 Schema。
软删除:删是标记,不是抹除
用户删了帖子,管理员还要恢复;报表要统计历史数据;法规要求数据保留若干年。这时候「真删」是灾难。软删除的思路:不删行,打标记。deletedAt 为 null 表示存活,有值表示已删除。
字段与索引
每个需要软删的模型加一个可空时间字段,并配上索引:
model User {
id String @id @default(cuid())
email String @unique
deletedAt DateTime?
@@index([deletedAt])
}
索引很关键。没有它,过滤已删数据的查询会全表扫描,数据量上来后越来越慢。
全局 scope:所有查询自动过滤
手写 where: { deletedAt: null } 太容易漏。用客户端扩展(Client Extensions)的 query 组件统一注入,应用代码从此无感:读到的永远只有存活数据(paranoid mode)。扩展的完整写法(v7 实例化、findMany/findFirst 注入、delete 变软删、恢复、绕过)见第 36 章,这里只讲生产决策:
- 管理后台要查已删数据或做硬删,保留一个未扩展的原始客户端。
- 扩展客户端是默认出口,原始客户端是特权出口,两个都要存在(下文 xprisma 指第 36 章扩展出的客户端,prisma 指未扩展的原始客户端)。
恢复、硬删与归档
恢复就是清标记,同时记审计日志:
await xprisma.user.update({
where: { id },
data: { deletedAt: null },
});
硬删留给两类场景:GDPR 删除权(用户要求彻底抹除)和保留期后的清理任务。比如定期任务把删除超过 90 天的订单真正删掉:
const cutoff = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000);
await prisma.order.deleteMany({
where: { deletedAt: { lt: cutoff } },
});
归档是硬删的温和替代:删除的行先搬进归档表(或 PG 分区),再清出主表;更冷的数据导出到 S3/Glacier 之类的冷存储。
审计字段模板
生产模型几乎都长一个样。把通用字段做成模板,每个模型照抄:
model Post {
id String @id @default(cuid())
title String
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
deletedAt DateTime? @map("deleted_at")
@@map("posts")
@@index([deletedAt])
}
createdAt 记录创建,updatedAt 由 @updatedAt 自动维护,deletedAt 留给软删。多租户表把索引换成 @@index([tenantId, deletedAt]),过滤更快。
500 行红线与按域拆分
一个 schema.prisma 超过 500 行,就该拆了。Prisma 支持多文件 Schema:入口文件保留生成器和数据源,业务模型按域拆分:
prisma/
├── schema.prisma # generator + datasource
├── auth.prisma # 用户、角色
├── billing.prisma # 订单、支付
└── content.prisma # 文章、评论
@map 解耦命名
数据库列名和 TypeScript 字段名不必一致。@map/@@map 把两边解耦:代码里用驼峰,数据库里用下划线或既有命名。老库迁移、团队命名规范冲突,都靠它化解。
多租户三模式
SaaS 的租户隔离有三条路线,没有银弹:
| 模式 | 做法 | 适合 | 代价 |
|---|---|---|---|
| 共享 Schema | 每行一个 tenantId 列 | 大量小租户,成本最低 | 隔离靠应用层或 RLS,风险最高 |
| Schema-per-tenant | 每租户一个数据库 Schema | 中型租户,有合规要求 | 依赖 multiSchema,管理复杂 |
| DB-per-tenant | 每租户一个数据库 | 企业级强隔离 | 成本最高,客户端按租户建 |
共享 Schema 最常用:便宜、好扩展,但必须配第 44 章的 RLS。DB-per-tenant 隔离最硬,代价是每租户一个 PrismaClient(见第 46 章工厂模式)。
Schema-per-tenant 用多 Schema(multiSchema)特性。PostgreSQL、CockroachDB、SQL Server 支持把表分组到命名空间:
generator client {
provider = "prisma-client"
output = "../generated/prisma"
}
datasource db {
provider = "postgresql"
schemas = ["public", "tenant_a"]
}
model TenantAOrder {
id Int @id
@@schema("tenant_a")
}
模型用 @@schema 归属,跨 Schema 查询照常,连接字符串里的 schema 变成默认 Schema。注意:multiSchema 是正式可用(GA)特性(v6.13.0 起),SQLite 和 MySQL 不支持。
外部托管表:查得到,管不着
Auth0、Clerk 这类服务自己管理用户表。Prisma 想查它,但不想让 Migrate 碰它。把表声明为外部托管表(externally managed tables,预览功能),在 prisma.config.ts 里登记:
export default defineConfig({
schema: "prisma/schema.prisma",
datasource: { url: env("DATABASE_URL") },
experimental: { externalTables: true },
tables: { external: ["public.users"] },
});
之后 prisma.user.findMany() 照常可用,migrate dev/deploy 完全跳过这张表。表结构变了,重新 db pull 同步即可。
Note外部托管表是 Preview 特性。与外部表建立关系时,需要在 migrations.initShadowDb 里提供占位 SQL,让影子数据库知道它的结构。
参考来源
- Prisma 官方文档:Multi-schema(GA)/ External tables(Preview 特性页)
- Prisma 官方文档:Best practices(schema design 章节)
- Mapagam:Implementing Soft Delete Pattern / Working with Multiple Schemas
- HireNodeJS:Prisma ORM for Node.js: The Complete Production Guide 2026