实体模型定义了应用程序中的数据结构和关系。它们是数据库表的抽象表示,框架使用EntityFramework Core来处理数据访问和操作,本篇文章将指引开发者如何更规范的定义实体模型。
EF Core 支持两种方式去定义实体模型中与数据库相关的配置:
OnModelCreating方法中使用代码配置。其中,在Fluent API的基础上,可使用IEntityTypeConfiguration接口将实体配置拆分到单独的类中。
请根据团队习惯选择合适的方式,重要的是保持一致性,这里提供两种常见的实践:
IEntityTypeConfiguration对每个实体进行配置。Fluent API(如关系,转换器,Json等)。在开发约定与规范中,有给出定义实体模型的规范和约定,这是建议但不强制的,如果没有充分的理由,建议遵循这些约定。
不同模块的实体要分文件夹,且命名空间要对应。
所有模型属性需要注释,所有枚举都要添加[Description]特性说明。
所有实体类默认继承自EntityBase,该类定义了Id、CreatedTime、UpdatedTime,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是一个抽象基类,默认实体都继承自它,它定义了一些常用的属性:
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; } }
从中我们可以看出:
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来明确配置关系,尤其是在一对一和多对多的中间表关系,这里遵循官方的定义方式即可。
除此之外,根据实践经验,建议遵循以下约定:
如博客与用户关联:
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和数组(Postgres)等数据库提供的数据类型,避免创建过多的关联表,将能够有效的避免产生复杂性。
EF Core对Json列的查询已经提供了非常良好的支持。
在实际应用中,通常我们会拆分多个模块,在一定程度上实现关注点分离,以便保持结构清晰,更好的开发和维护。
但很多时候,实体模型之间是跨模块关联的,那么应该如何定义关联关系呢?
通常我们会将业务相近的实体在一个模块中定义,在同一模块中的实体,建议定义导航属性,也就是数据库中包含外键约束。
对于像用户模块,其他模块中的实体很多都需要关联用户,那么在其他实体定义时,可以只定义UserId属性,而不定义导航属性User。
Tip
在可能的情况下,最好使用外键约束,这样可以确保数据的完整性和一致性。在必要的时候,你可以去除外键约束。
如果你一开始没有外键约束,再添加外键约束可能会非常麻烦。