← 返回首页

EF Core 软删除与唯一索引的一次踩坑

· C# / EF Core

项目里商品、供应商这类档案用的是软删除(打 IsDeleted 标记,查询时过滤)。为了保证「编码不重复」,很自然地在唯一索引里把 IsDeleted 也放进去,结果线上报了数据库唯一键冲突。这篇记录原因和修法。

问题:bool 只有两个值

唯一索引列里包含 IsDeleted 时,考虑这个序列:

  1. 编码 A001 的记录删除(IsDeleted = 1);
  2. 用 A001 重建一条(IsDeleted = 0),不冲突;
  3. 新建的这条再删除(IsDeleted = 1)—— 和第 1 步那条已删除记录在索引列上完全相同,撞唯一键。

也就是说同一条数据只要经历「删除 → 重建 → 再删除」,第二次删除必然失败。bool 只有两个值,这条路注定走不通。

修法:过滤索引

正确姿势是唯一索引列里去掉 IsDeleted,改用数据库的过滤索引,只对未删除的行生效。PostgreSQL 写法:

builder.HasIndex(x => new { x.TenantId, x.Code })
       .IsUnique()
       .HasFilter("\"IsDeleted\" = false");

效果:未删除的行之间编码唯一,已删除的行不参与唯一性判断,删除多少条都不冲突。

两个附带注意点

小结

软删除的唯一性约束,本质是「只在活着的行里唯一」,这个语义天然应该用过滤索引表达,而不是把删除标记塞进索引列。判断标准很简单:如果两个已删除的行在索引列上可能撞车,这个索引设计就是错的。