Blogs
📆 2026-01-11 16:13

实体模型定义

实体模型定义了应用程序中的数据结构和关系。它们是数据库表的抽象表示,框架使用EntityFramework Core来处理数据访问和操作,本篇文章将指引开发者如何更规范的定义实体模型。

风格一致性

EF Core 支持两种方式去定义实体模型中与数据库相关的配置:

  • 数据注解(Data Annotations),即使用特性标签。
  • Fluent API,在OnModelCreating方法中使用代码配置。

其中,在Fluent API的基础上,可使用IEntityTypeConfiguration接口将实体配置拆分到单独的类中。

请根据团队习惯选择合适的方式,重要的是保持一致性,这里提供两种常见的实践:

  • 全部使用IEntityTypeConfiguration对每个实体进行配置。
  • 使用数据注解进行简单配置(如长度,主键等),复杂配置使用Fluent API(如关系,转换器,Json等)。

约定与规范

在开发约定与规范中,有给出定义实体模型的规范和约定,这是建议但不强制的,如果没有充分的理由,建议遵循这些约定。

  • 不同模块的实体要分文件夹,且命名空间要对应。

  • 所有模型属性需要注释,所有枚举都要添加[Description]特性说明。

  • 所有实体类默认继承自EntityBase,该类定义了IdCreatedTimeUpdatedTime,IsDeleted常用属性。

  • Id属性默认使用Guid类型,客户端Guid V7生成。

  • string类型,要定义最大长度,除非明确不限长度。

  • decimal类型,精度和小数位数需要明确。

    • 对于较小范围的decimal类型,建议使用decimal(10, 2)

    • 对于较大范围的decimal类型,建议使用decimal(18, 6)

      [Column(TypeName = "decimal(10,2)")]
      public decimal TotalPrice { get; set; }
      
  • 所有枚举值必须添加[Description]特性。

  • 时间类型使用DateTimeOffset,而不是DateTime,以确保时间信息是完整的。

  • 明确仅为日期的属性,使用DateOnly类型。

  • 明确仅为时间的属性,使用TimeOnly类型。

EntityBase

EntityBase是一个抽象基类,默认实体都继承自它,它定义了一些常用的属性:

public abstract class EntityBase
{
    [Key]
    public Guid Id { get; set; } = Guid.CreateVersion7();
    public DateTimeOffset CreatedTime { get; private set; } = DateTimeOffset.UtcNow;
    public DateTimeOffset UpdatedTime { get; set; }
    public bool IsDeleted { get; set; }
    public Guid TenantId { get; set; }
}

从中我们可以看出:

  • 定义了默认主键Id,是客户端生成的有序GUID,
  • 定义创建时间和默认值,只要从实体创建时,就会自动赋值为当前UTC时间。
  • 定义了更新时间属性,在更新时自动赋值为当前UTC时间。
  • 定义了软删除属性IsDeleted,框架默认会过滤掉软删除的数据。
  • TenantId可根据实际需求选择是否继承。

唯一约束

在设计实体时必须要考虑实体的唯一性,并且要添加唯一约束,唯一约束能够有效防止数据重复和冲突。

ManagerBase中的UpsertAsync方法会根据主键来判断是插入还是更新数据,然后根据唯一约束来防止重复数据的插入。

由于系统默认实现租户模式,所以统一处理了TenantId的索引。也就是说你不需要为每个实体添加TenantId到唯一约束中(添加也没关系),系统会自动帮你处理。同时,对于唯一约束,会自动添加HasFilter过滤索引,过滤掉软删除的数据。

Tip

你可以在ContextBase.cs中的ConfigureMultiTenantUniqueIndexes方法中,修改默认实现。

乐观并发

对于需要处理并发更新的实体,建议使用乐观并发控制,通过在实体中添加一个属性来实现。

[Timestamp]
public byte[] RowVersion { get; set; }

以上可以兼容多种数据库,如果想利用数据库自身的特性,如Postgres的xmin,可以这样定义:

[Timestamp]
public unit RowVersion { get; set; }

关系映射

EF Core通过导航属性能够自动识别一对多和多对多关系,我们也可以使用Fluent API来明确配置关系,尤其是在一对一和多对多的中间表关系,这里遵循官方的定义方式即可。

除此之外,根据实践经验,建议遵循以下约定:

显示定义关联Id属性

如博客与用户关联:

public class Blog : EntityBase
{
    public string Title { get; set; }
    // 显示定义关联Id属性
    public Guid UserId { get; set; }
    [ForeignKey(nameof(UserId))]
    public User User { get; set; }
}

虽然UserId属性不是必须的,但强烈建议显示定义,这会让后续的查询更加灵活和方便。

多对多的关系,建议显示定义中间表实体,并显示定义关联Id属性

使用Json和数组简化关系

适当使用Json和数组(Postgres)等数据库提供的数据类型,避免创建过多的关联表,将能够有效的避免产生复杂性。

EF Core对Json列的查询已经提供了非常良好的支持。

模块间关联关系

在实际应用中,通常我们会拆分多个模块,在一定程度上实现关注点分离,以便保持结构清晰,更好的开发和维护。

但很多时候,实体模型之间是跨模块关联的,那么应该如何定义关联关系呢?

通常我们会将业务相近的实体在一个模块中定义,在同一模块中的实体,建议定义导航属性,也就是数据库中包含外键约束。

对于像用户模块,其他模块中的实体很多都需要关联用户,那么在其他实体定义时,可以只定义UserId属性,而不定义导航属性User

Tip

在可能的情况下,最好使用外键约束,这样可以确保数据的完整性和一致性。在必要的时候,你可以去除外键约束。

如果你一开始没有外键约束,再添加外键约束可能会非常麻烦。