3. 定义实体模型与DbContext
在 EF Core 中,实体模型是数据库结构的“代码化表达”。一个类要成为实体,必须能被 DbContext 识别;一个属性要成为主键、外键或映射到特定列,必须通过约定、数据注解或 Fluent API 明确表达。本节围绕“如何把 C# 类准确映射到数据库表”展开,重点讲清三种配置方式的优先级、主键与外键的配置方法,以及表映射的常见场景。
一、实体模型与 DbContext 的关系
DbContext 是 EF Core 的会话入口,它通过 DbSet<T> 暴露实体集合,并在运行时根据实体类型构建模型元数据。实体类本身可以是普通 POCO 类,不需要继承任何基类。例如一个订单系统的核心实体可以这样定义:
public class Order
{
public int OrderId { get; set; }
public string OrderNo { get; set; } = string.Empty;
public DateTime CreatedAt { get; set; }
public int CustomerId { get; set; }
public Customer? Customer { get; set; }
public List<OrderItem> Items { get; set; } = new();
}
public class Customer
{
public int CustomerId { get; set; }
public string Name { get; set; } = string.Empty;
public List<Order> Orders { get; set; } = new();
}
DbContext 中通过 DbSet 暴露这些实体:
public class AppDbContext : DbContext
{
public DbSet<Order> Orders => Set<Order>();
public DbSet<Customer> Customers => Set<Customer>();
protected override void OnConfiguring(DbContextOptionsBuilder options)
=> options.UseSqlServer("Server=.;Database=ShopDb;Trusted_Connection=True;");
}
此时 EF Core 会依据“约定”推断:名为 Id 或 <类型名>Id 的属性是主键;名为 CustomerId 的属性是外键;类名复数化后作为表名。约定能覆盖大部分简单场景,但一旦命名不符合约定、需要复合主键、需要指定列类型或索引,就必须显式配置。
二、三种配置方式与优先级
EF Core 提供三种配置途径:约定、数据注解、Fluent API。它们的作用范围与优先级不同:约定最弱,数据注解次之,Fluent API 最强。当同一属性被多种方式配置时,Fluent API 会覆盖数据注解,数据注解会覆盖约定。因此,推荐的做法是:简单映射用数据注解,复杂关系、复合键、索引、级联行为统一放在 OnModelCreating 的 Fluent API 中,避免配置分散。
三、主键配置
单主键最简单:属性名为 Id 或 <类名>Id 时约定自动识别。若属性名不符合约定,可用数据注解:
public class Product
{
[Key]
public int Pid { get; set; }
public string Name { get; set; } = string.Empty;
}
复合主键无法用 [Key] 表达,必须使用 Fluent API:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<OrderItem>()
.HasKey(oi => new { oi.OrderId, oi.ProductId });
}
主键默认是自增的(SQL Server 下为 IDENTITY)。若需手动赋值,可关闭自增:
modelBuilder.Entity<Order>()
.Property(o => o.OrderId)
.ValueGeneratedNever();
四、外键与导航属性配置
外键配置的核心是“关系两端”。EF Core 通过导航属性 + 外键属性推断关系,但推断可能出错,尤其是多个外键指向同一实体时。此时必须显式配置。例如 Order 与 Customer 是一对多关系:
modelBuilder.Entity<Order>()
.HasOne(o => o.Customer)
.WithMany(c => c.Orders)
.HasForeignKey(o => o.CustomerId)
.OnDelete(DeleteBehavior.Restrict);
这里 HasOne 指定主体端,WithMany 指定集合端,HasForeignKey 明确外键属性,OnDelete 控制删除行为。默认级联删除在多数业务中过于危险,建议对重要关系显式设为 Restrict 或 SetNull。若使用数据注解,可在导航属性上标注:
public class Order
{
public int OrderId { get; set; }
public int CustomerId { get; set; }
[ForeignKey(nameof(CustomerId))]
public Customer? Customer { get; set; }
}
对于一对一关系,需在从端使用 HasForeignKey 并确保外键唯一;对于多对多,EF Core 5+ 支持隐式连接表,也可用 UsingEntity 显式命名连接表并配置额外字段。
五、表映射与列映射
表名、架构、列名、列类型、长度、是否可空、索引等都属于表映射。数据注解方式:
[Table("T_Orders", Schema = "sales")]
public class Order
{
[Column("order_no", TypeName = "varchar(50)")]
public string OrderNo { get; set; } = string.Empty;
[MaxLength(200)]
public string? Remark { get; set; }
}
Fluent API 方式更集中,也支持索引、默认值、计算列等注解无法表达的配置:
modelBuilder.Entity<Order>(entity =>
{
entity.ToTable("T_Orders", "sales");
entity.Property(o => o.OrderNo)
.HasColumnName("order_no")
.HasColumnType("varchar(50)")
.IsRequired();
entity.Property(o => o.CreatedAt)
.HasDefaultValueSql("GETUTCDATE()");
entity.HasIndex(o => o.OrderNo).IsUnique();
});
需要注意:HasColumnType 与 HasMaxLength 同时使用时,类型名优先;若只写 HasMaxLength(50),SQL Server 会生成 nvarchar(50)。对于 decimal 精度,应使用 HasPrecision(18, 2),否则可能得到默认精度导致金额截断。
六、配置落地检查清单
完成实体与 DbContext 配置后,建议按以下清单核对,避免运行时才发现映射错误:
- 每个实体是否都有主键?复合主键是否用 Fluent API 声明?
- 外键属性与导航属性是否成对出现?多外键指向同一实体时是否显式指定?
- 删除行为是否符合业务预期?是否误用了默认级联删除?
- 表名、架构、列名是否与既有数据库一致?列类型与长度是否匹配?
- 金额、日期等字段是否指定了精度或默认值?
- 常用查询列是否建立了索引?唯一约束是否通过 IsUnique 表达?
dotnet ef dbcontext info 查看 DbContext 基本信息,用 dotnet ef migrations add Init 生成迁移,再通过迁移脚本核对生成的表结构、列类型、外键与索引是否符合预期。迁移文件是验证模型配置最直接的依据。总结来说,实体模型配置的关键在于:先用约定减少重复代码,再用数据注解处理简单映射,最后把复杂关系、复合键、索引和删除行为集中到 Fluent API。三者配合,才能让 C# 类与数据库表之间形成稳定、可维护、可验证的映射关系。