4、Fluent API 与数据注解:Fluent API详解、数据注解属性、两种方式的取舍、高级映射配置

4.1 两种配置方式,你选哪个?

做EF Core开发,配置实体和数据库的映射关系,主要有两条路:数据注解Fluent API

数据注解,就是在实体类的属性上直接贴标签。比如 [Key][Required][MaxLength(50)]。简单直观,一看就懂。

Fluent API,则是通过代码来配置。在 DbContextOnModelCreating 方法里写配置逻辑。比如:

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

但是,数据注解有个硬伤:它污染了你的实体类。你想想看,一个纯粹的领域模型,上面挂满了数据库相关的标签。这违反了单一职责原则。

我曾经在一个项目里,实体类上堆了十几个注解。后来业务变了,要改映射关系。我翻遍了所有实体类,一个个改注解。那感觉,就像在垃圾堆里找钥匙。从那以后,复杂映射我坚决用Fluent API。

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 的完整配置能力。数据注解只是它的一个子集。

我的建议:能用数据注解搞定的简单映射,就用数据注解。遇到复杂场景,比如多对多、继承映射、值对象,果断切到 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,用来区分是狗还是猫。简单高效。

避坑指南:TPH 虽然简单,但如果子类字段差异很大,会产生大量 NULL 列。这时候可以考虑 TPT(每个子类一张表)。不过 TPT 性能稍差,因为要 JOIN。我一般默认用 TPH,除非字段差异真的很大。

4.5 两种方式的取舍:我的实战经验

说了这么多,到底怎么选?我总结了几条原则:

  • 团队习惯:如果团队都熟悉数据注解,小项目直接用。别为了炫技而用 Fluent API。
  • 配置复杂度:超过3个注解的属性,我建议改用 Fluent API。否则实体类太臃肿。
  • 可测试性:Fluent API 的配置可以单独提取到配置类中,方便单元测试。数据注解不行。
  • 迁移友好:Fluent API 的配置变更,EF Core 迁移能自动识别。数据注解的某些变更(比如改列名),迁移可能漏掉。

我记得有一次,接手一个遗留项目。实体类上全是数据注解,但业务逻辑又混在里面。我想加个全局软删除过滤器,发现根本没法加。最后花了三天,把所有注解迁移到 Fluent API。那三天,我发誓以后再也不在实体类上堆注解了。

所以,我的最终建议是:默认用 Fluent API,简单场景用数据注解。两者不冲突,可以共存。但如果你只能选一种,我选 Fluent API。因为它更干净,更强大,更可控。

好了,这一章就到这里。下一章我们聊聊「导航属性与关系配置」,那是 EF Core 的灵魂所在。