首页 / Prisma ORM 入门教程 / 软删除模式与生产 Schema 设计

Prisma ORM 入门教程

软删除模式与生产 Schema 设计

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

软删除deletedAtGDPR生产Schema多租户multiSchema外部托管表审计字段

本节目标:掌握软删除的完整生命周期(标记、过滤、恢复、硬删、归档),并学会设计生产级 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