深夜十一点半,我刚准备合上电脑,手机弹出一条线上告警:订单主表里出现了两条一模一样的订单号。第一反应是不可能——代码里明明做过唯一性判断。可打开数据库一查,两条记录静静地躺在那里,连创建时间都只差了0.3秒。这不是第一次了。后来我复盘了整个项目的Entity Framework Core数据链路,发现问题根本不在“有没有写判断”,而在于我从来没有认真对待过一个词:数据完整性。
Entity Framework Core(下面我直接叫EF Core)是.NET生态里绕不开的ORM框架,但很多人对它的认知还停留在“少写点SQL”这个层面。真正把EF Core用出价值,恰恰要看你在数据完整性上做了多少功课:外键关系怎么约束、并发时怎么防止互相覆盖、删除一条数据会不会连带误删、谁在什么时间改了什么数据能不能追溯。这篇文章我不打算讲教科书,就用我自己踩过的坑和补过的洞,把EF Core里保证数据完整性的完整实践拆给你看。适合正在用EF Core做业务系统、被数据错乱问题折腾过的开发者,也适合准备从0设计一个可维护数据模型的朋友参考。
1. 先想清楚:EF Core里的“数据完整性”到底指什么
很多人一提数据完整性,第一反应就是“数据库里的外键和主键约束”。这话没错,但放到EF Core这个语境下,它只是冰山一角。EF Core的数据完整性是一个贯穿“模型定义—迁移同步—事务提交—并发控制—删除策略—审计追溯”的工程链路。我习惯把完整性拆成四个层次来理解。
1.1 四类完整性约束与EF Core的映射
第一类是实体完整性,解决的是“每一行数据都能被唯一识别”的问题。对应到EF Core里就是主键设计,无论是默认的Id约定、Guid主键还是业务主键,都要保证在表内不重复。
第二类是引用完整性,解决的是“表与表之间的关系不能悬空”的问题。比如订单明细里的OrderId必须指向一个真实存在的订单,不能出现“孤儿数据”。EF Core里通过外键属性、导航属性和关系配置来实现。
第三类是域完整性,解决的是“字段值必须符合业务规则范围”的问题。比如价格不能为负数、手机号不能超过11位、状态字段不能填一个未知枚举值。EF Core里用数据注解或Fluent API,配合数据库列的精确类型来约束。
第四类是用户自定义完整性,也就是业务自己定义的规则。比如一个用户在同一时间段内不能重复参加同一个活动、一个SKU在同一个仓库里库存记录唯一,这类规则通常通过唯一索引、组合唯一约束甚至数据库触发器实现。
第四类完整性有几个典型的坑。
EF Core中配置一个组合唯一索引,很多人会这样写:
csharp复制modelBuilder.Entity<OrderItem>()
.HasIndex(x => new { x.OrderId, x.SkuId })
.IsUnique();
这个配置生成的SQL里确实有唯一索引,但只要你在应用层没有提前捕获DbUpdateException并做友好提示,用户就会看到一条“无法保存更改”的原始报错。更隐蔽的问题是,如果你只在应用层用“先查询再插入”的方式判断是否重复,在高并发场景下极大概率出现竞态问题——两个请求同时查到“不存在”,同时插入,然后数据库约束炸掉。我在文章后面会专门讲这条链路的正确姿势。
1.2 为什么不能只靠数据库约束,也不能只靠应用层校验
这是一个很容易走极端的点。刚入门的同学觉得“数据库有约束就够了,EF Core模型随便写写也没关系”,结果模型里定义的可空字段和数据库的非空字段不一致,等到执行迁移或者线上插入数据时才报错。另一端是过于依赖应用层校验,Everywhere写满if判断,但并发一高就漏。
我的原则很明确:EF Core模型约束负责“友好提示”,数据库约束负责“最终兜底”,迁移脚本负责“保持一致”。模型上标注必填、长度、精度,用户在UI层收到的是清晰的中文错误信息;数据库层则通过非空、外键、唯一索引、Check约束,确保任何绕过应用层的写入也无法破坏数据。这两者不是二选一,而是同一道防线的两道闸门。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 从模型设计开始:把完整性写进实体定义里
数据完整性不是等表建好之后再补的,而是在你写第一个实体类的时候就已经决定了。我见过太多项目,实体类里全是string类型的属性,什么约束标注都没有,全靠数据库迁移时顺手改几列。结果就是模型和数据库各说各话,最后连EF Core自己都分不清哪个才是准的。
2.1 一套可以直接抄的属性约束规范:数据注解 vs Fluent API
EF Core里配置约束有两种主流方式:数据注解(Data Annotations)和Fluent API。数据注解写起来简单直观,直接贴在实体属性上,适合快速开发;Fluent API集中在OnModelCreating里统一管理,可读性更强,而且能配置数据注解搞不定的高级约束,比如组合唯一索引、级联删除行为、行版本并发令牌。
我的建议是:能用Fluent API就优先用Fluent API,尤其项目规模一大,所有的约束集中在同一个地方排查起来极其舒服。数据注解的典型写法是这样:
csharp复制public class Product
{
public int Id { get; set; }
[Required]
[MaxLength(50)]
public string Sku { get; set; }
[Required]
[MaxLength(200)]
public string Name { get; set; }
[Column(TypeName = "decimal(18,2)")]
public decimal Price { get; set; }
}
Fluent API的等价写法是这样的:
csharp复制protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Product>(entity =>
{
entity.Property(x => x.Sku)
.IsRequired()
.HasMaxLength(50);
entity.Property(x => x.Name)
.IsRequired()
.HasMaxLength(200);
entity.Property(x => x.Price)
.HasPrecision(18, 2);
entity.HasIndex(x => x.Sku)
.IsUnique();
});
}
这里有个细节很多人没
