LOADING
1405 words
7 minutes
数据建模的核心决策(三)

数据建模比写 CRUD 更重要——不是因为 CRUD 简单,而是因为 Schema 一旦应用到生产数据库,修改成本远高于改代码。这一篇不讲 Prisma 语法,讲三个在定义 schema.prisma 之前就需要想清楚的决策。


一、约束放在数据库还是代码

代码检查为什么不够

用户不能对同一篇文章点两次赞。你可以在代码里做这个检查:

const existing = await prisma.postLike.findUnique({
where: { userId_postId: { userId, postId } }
});
if (existing) return { alreadyLiked: true };
await prisma.postLike.create({ data: { userId, postId } });

这段代码在单用户测试时完美运行。生产环境里,用户快速双击点赞按钮,两个请求几乎同时到达:请求 A 查不到已有记录 → 请求 B 也查不到(A 还没写入)→ 两个请求都通过了检查 → 两个请求都执行了 create

代码可以防止 99% 的问题,但并发场景下的那个 1% 只靠代码挡不住。

数据库约束是最后防线

model PostLike {
userId String
postId String
user User @relation(fields: [userId], references: [id])
post Post @relation(fields: [postId], references: [id])
@@id([userId, postId]) // 复合主键 = 数据库保证唯一
}

@@id([userId, postId]) 在数据库层面创建了复合唯一约束。两个请求中必有一个会触发唯一约束冲突(Prisma 抛 P2002 错误)。你在代码里捕获这个错误,返回”已点赞”——不是 500 错误,是正常的业务分支:

try {
await prisma.postLike.create({ data: { userId, postId } });
} catch (error) {
if (error.code === "P2002") {
return { alreadyLiked: true }; // 正常分支,不是报错
}
throw error;
}

什么放数据库,什么放代码

放数据库:唯一约束(邮箱不能重复、同一用户不能对同一文章点两次赞)、外键约束(Post 的 authorId 必须指向存在的 User)、非空约束(标题不能为空)。

放代码:复杂的业务规则(用户连续签到 7 天才能获得徽章)、跨表的条件约束(VIP 用户可以创建无限篇文章)。

判断标准:如果约束是”这个字段不能重复”或”这个字段必须指向有效记录”——放数据库。如果是”在某种业务条件下才允许”——放代码。


二、级联删除:删用户时,他的文章怎么办

四种策略,一个问题

onDelete 有四个选项,但它们回答的是同一个问题:当父记录被删除时,子记录还有没有独立存在意义。

Cascade(级联删除):删用户,他的文章、评论、点赞一起删。适用于子记录离开父记录就没有意义的场景——一条评论离开它所属的文章就没有上下文。删文章时评论一起消失是合理的。

Restrict(拒绝删除):如果用户还有文章,数据库拒绝删除用户。适用于需要先手动处理关联数据的场景——一个用户有未完成订单时不应该被删除。

SetNull(设为空):删用户后,他的文章保留,authorId 变成 NULL。文章还在,但作者信息丢失——适合”内容应该保留但关联可断”的场景。

NoAction:数据库不自动做任何操作,但外键约束还在——如果子记录还存在引用,删除会失败。效果类似 Restrict。

选择标准是时间维度

六个月后你删除一个用户。那时候还有人记得”这个用户有三篇文章”吗?如果没人记得,Cascade 是最干净的选择——一次删除,所有关联数据清理干净。如果数据分析团队还在查询历史文章的作者分布,SetNull 保留文章但断开关联更合适。


三、Schema 变更不该是自动化的

为什么不能在部署时自动改表结构

如果一个后端服务在启动时自动执行 prisma migrate dev 来修改数据库结构,出问题时你不知道是谁改的、什么时候改的、为什么要改——因为这个变更发生在部署脚本里,没有经过人审阅。

Schema 变更的正确流程是:

  1. 在 Schema 文件里改——加字段、改类型、删关系
  2. 生成 Migration SQL——prisma migrate dev 自动生成 SQL 文件,放在 prisma/migrations/ 目录下
  3. 人工审阅生成的 SQL——不是信任自动生成的 SQL,而是逐项检查。加字段是 ALTER TABLE ADD COLUMN,没问题。删字段是 DROP COLUMN——这个 SQL 在生产环境跑会立刻删掉一列数据。所以你会手动修改 Migration SQL,改成先标记废弃、确认无引用后再删
  4. 提交到 Git——Migration SQL 和 Schema 文件一起提交。任何部署都能从 Git 历史看到数据库结构是怎么一步步变成今天这样的
  5. 部署时执行——prisma migrate deploy 只执行尚未应用的 Migration,不会现场生成新的结构变更

这五步的核心原则是:Schema 变更是显式的、可追溯的、有人审阅的。


小结

  • 数据库约束是最后防线——代码检查挡不住并发写入
  • 级联删除的选择取决于”子记录有没有独立存在意义”——不是因为”Cascade 最方便”
  • Schema 变更不应该是自动化的——Migration 是显式的、可审阅的、可追溯的

下一篇:查询设计的三个陷阱(四)——N+1 问题为什么是最容易被忽视的性能杀手、offset 分页的真实代价、以及 select 和 include 的选择标准。

数据建模的核心决策(三)
/posts/2026-7-24/3-prisma数据建模/
Author
Atopos
Published at
2026-07-24
License
CC BY-NC-SA 4.0

Some information may be outdated