数据建模比写 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 变更的正确流程是:
- 在 Schema 文件里改——加字段、改类型、删关系
- 生成 Migration SQL——
prisma migrate dev自动生成 SQL 文件,放在prisma/migrations/目录下 - 人工审阅生成的 SQL——不是信任自动生成的 SQL,而是逐项检查。加字段是
ALTER TABLE ADD COLUMN,没问题。删字段是DROP COLUMN——这个 SQL 在生产环境跑会立刻删掉一列数据。所以你会手动修改 Migration SQL,改成先标记废弃、确认无引用后再删 - 提交到 Git——Migration SQL 和 Schema 文件一起提交。任何部署都能从 Git 历史看到数据库结构是怎么一步步变成今天这样的
- 部署时执行——
prisma migrate deploy只执行尚未应用的 Migration,不会现场生成新的结构变更
这五步的核心原则是:Schema 变更是显式的、可追溯的、有人审阅的。
小结
- 数据库约束是最后防线——代码检查挡不住并发写入
- 级联删除的选择取决于”子记录有没有独立存在意义”——不是因为”Cascade 最方便”
- Schema 变更不应该是自动化的——Migration 是显式的、可审阅的、可追溯的
下一篇:查询设计的三个陷阱(四)——N+1 问题为什么是最容易被忽视的性能杀手、offset 分页的真实代价、以及 select 和 include 的选择标准。
Some information may be outdated