4、Fluent API 与数据注解:Fluent API详解、数据注解属性、两种方式的取舍、高级映射配置
4.1 两种配置方式,你选哪个?
做EF Core开发,配置实体和数据库的映射关系,主要有两条路:数据注解和Fluent API。
数据注解,就是在实体类的属性上直接贴标签。比如 [Key]、[Required]、[MaxLength(50)]。简单直观,一看就懂。
Fluent API,则是通过代码来配置。在 DbContext 的 OnModelCreating 方法里写配置逻辑。比如:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>(entity =>
{
entity.HasKey(e => e.BlogId);
entity.Property(e => e.Url).IsRequired().HasMaxLength(200);
});
}
我个人习惯,小项目用数据注解,图快。大项目或者需要精细控制时,我偏向Fluent API。为什么?往下看。
4.2 数据注解:简单粗暴,但有边界
数据注解用起来确实爽。一行代码搞定一个约束。常用的有这些:
| 注解 | 作用 | 示例 |
|---|---|---|
| [Key] | 标记主键 | [Key] public int Id { get; set; } |
| [Required] | 字段不可为空 | [Required] public string Name { get; set; } |
| [MaxLength(100)] | 最大长度 | [MaxLength(100)] public string Title { get; set; } |
| [Column("user_name")] | 指定列名 | [Column("user_name")] public string UserName { get; set; } |
| [ForeignKey("CategoryId")] | 指定外键 | [ForeignKey("CategoryId")] public Category Category { get; set; } |
| [Index(nameof(Email), IsUnique = true)] | 创建索引 | [Index(nameof(Email), IsUnique = true)] public class User |
但是,数据注解有个硬伤:它污染了你的实体类。你想想看,一个纯粹的领域模型,上面挂满了数据库相关的标签。这违反了单一职责原则。
4.3 Fluent API:灵活,但需要多写几行
Fluent API 的好处是:配置和实体分离。你的实体类干干净净,所有映射逻辑集中在 OnModelCreating 里。维护起来,一目了然。
而且,有些配置数据注解根本做不到。比如:
- 复合主键:
entity.HasKey(e => new { e.BlogId, e.PostId }); - 级联删除行为:
entity.HasMany(...).WithOne(...).OnDelete(DeleteBehavior.Cascade); - 字段精度:
entity.Property(e => e.Price).HasPrecision(18, 2); - 全局查询过滤器:
entity.HasQueryFilter(e => !e.IsDeleted);
说白了,Fluent API 是 EF Core 的完整配置能力。数据注解只是它的一个子集。
4.4 高级映射配置:Fluent API 的杀手锏
这里我重点讲几个高级用法。你在面试或实际项目中,肯定会碰到。
4.4.1 值对象(Owned Entity)
比如用户有地址,地址不是独立实体,而是用户的一部分。用 Fluent API 配置:
modelBuilder.Entity<User>(entity =>
{
entity.OwnsOne(e => e.Address, address =>
{
address.Property(a => a.Street).HasColumnName("Street").HasMaxLength(100);
address.Property(a => a.City).HasColumnName("City").HasMaxLength(50);
});
});
这样,地址字段会直接映射到 User 表里,列名是 Street 和 City。干净利落。
4.4.2 表拆分(Table Splitting)
有时候,一个表对应多个实体。比如订单表拆成订单头和订单详情两个实体。配置方式:
modelBuilder.Entity<Order>().ToTable("Orders");
modelBuilder.Entity<OrderDetail>().ToTable("Orders");
modelBuilder.Entity<Order>(entity =>
{
entity.HasOne(e => e.Detail).WithOne()
.HasForeignKey<OrderDetail>(e => e.OrderId);
});
嗯,这里要注意:两个实体共享同一张表,必须有一对一关系。我在做电商项目时用过这个技巧,减少了不必要的JOIN。
4.4.3 继承映射(TPH, TPT, TPC)
EF Core 支持三种继承映射策略。最常用的是 TPH(一张表存所有子类)。
modelBuilder.Entity<Animal>(entity =>
{
entity.HasDiscriminator<string>("AnimalType")
.HasValue<Dog>("Dog")
.HasValue<Cat>("Cat");
});
这样,Animal 表里会多一列 AnimalType,用来区分是狗还是猫。简单高效。
4.5 两种方式的取舍:我的实战经验
说了这么多,到底怎么选?我总结了几条原则:
- 团队习惯:如果团队都熟悉数据注解,小项目直接用。别为了炫技而用 Fluent API。
- 配置复杂度:超过3个注解的属性,我建议改用 Fluent API。否则实体类太臃肿。
- 可测试性:Fluent API 的配置可以单独提取到配置类中,方便单元测试。数据注解不行。
- 迁移友好:Fluent API 的配置变更,EF Core 迁移能自动识别。数据注解的某些变更(比如改列名),迁移可能漏掉。
我记得有一次,接手一个遗留项目。实体类上全是数据注解,但业务逻辑又混在里面。我想加个全局软删除过滤器,发现根本没法加。最后花了三天,把所有注解迁移到 Fluent API。那三天,我发誓以后再也不在实体类上堆注解了。
所以,我的最终建议是:默认用 Fluent API,简单场景用数据注解。两者不冲突,可以共存。但如果你只能选一种,我选 Fluent API。因为它更干净,更强大,更可控。
好了,这一章就到这里。下一章我们聊聊「导航属性与关系配置」,那是 EF Core 的灵魂所在。