实体
- 实体类
- 聚合根类
- 领域事件
- 常规接口
- 审计
- 软删除
- 激活/失活实体
- 实体改变事件
- IEntity接口
实体是DDD(领域驱动设计)的核心概念之一。Eric Evans描述它为"An object that is not fundamentally defined by its attributes, but rather by a thread of continuity and identity."所以,实体有Id并存储在数据库。实体一般映射到关系库中的表。
在ABP,实体继承自Entity类。如下示例:
public class Person : Entity { public virtual string Name { get; set; } public virtual DateTime CreationTime { get; set; } public Person() { CreationTime = DateTime.Now; } }
Persion类定义为实体。它有两个属性,实体类还定义了Id属性,它是实体的主键。所以,所有实体主键的名字都一样,为Id。
Id(主键)的类型可以改变,默认为int(Int32)类型。如果想定义其他类型作为Id,可以按如下所示显示的声明:
public class Person : Entity<long> { public virtual string Name { get; set; } public virtual DateTime CreationTime { get; set; } public Person() { CreationTime = DateTime.Now; } }
还可以将主键类型设置为String、Guid或其他类型。
实体类重写了相等运算符(==),可以方便的检查两个实体是否相等(Id相同)。实体还定义了IsTransient()方法检查是否有Id。
“在DDD中,聚合是一种模式。一个DDD聚合是领域对象群集,这些领域对象可以作为一个单元对待。例如订单和它的行项目,这些是独立的对象,但是把订单(和它的行项目)作为一个聚合对待是有用的。”(Martin Fowler -参见详细描述)。
虽然ABP不强制使用聚合,在应用中,我们或许想创建聚合和聚合根。ABP定义了AggregateRoot类,它扩展了实体为聚合创建聚合根实体。
聚合根定义了领域事件集合,通过聚合根类产生领域事件。这些事件在当前工作单元完成之前自动触发。实际上,任何实体可以通过实现IGeneratesDomainEvents接口来生成领域事件,但是,通常在(最佳实践)聚合根中生成领域事件。这就是为什么默认为聚合根而不是其他实体类。
在许多应用中,相似的实体属性(和数据库表字段)被使用,如CreationTime表示实体什么事件被创建。ABP提供了一些有用的接口使这些常见属性显示声明。这也提供了编写实体常用代码的一种方式,实现这些接口。
IHasCreationTime使实体拥有一个常用的"creation time"属性。当实现了这个接口的实体被插入到数据库时,ABP自动设置CreationTime为当前时间。
public interface IHasCreationTime { DateTime CreationTime { get; set; } }
ABP自动设置CreatorUserId为当前用户Id,当保存一个新实体的时候。也可以通过将实体继承CreationAuditedEntity类来实现ICreationAudited接口。它也有一个泛型版本,可以实现不同类型的Id属性。
修改也有类似的接口:
public interface IHasModificationTime { DateTime? LastModificationTime { get; set; } } public interface IModificationAudited : IHasModificationTime { long? LastModifierUserId { get; set; } }
当更新实体时,ABP自动设置这些属性。只需要在实体中定义他们。
如果想实现所有的审计属性,可以直接实现IAudited接口:
public interface IAudited : ICreationAudited, IModificationAudited { }
有一个捷径,可以直接继承AuditedEntity类而不是直接实现IAudited接口。AuditedEntity类也有一个泛型版本,以适应不同类型的Id属性。
注意:ABP从ABP会话中获取当前用户Id。
软删除是常用的模式,用来标记实体删除而不是真的从数据库中删除。例如,可能不想从数据库中硬删除一个用户,因为它和其他表有许多关系。ISoftDelete接口用来实现这个目的:
public interface ISoftDelete { bool IsDeleted { get; set; } }
ABP实现了开箱即用的软删除模式。当软删除实体被删除时,ABP会检测到并阻止删除,设置IsDeleted为true,然后更新实体到数据库。ABP不会从数据库中获取(select)软删除的实体,自动过滤他们。
如果使用软删除,也想记录实体什么时候被删除、谁删除,可以实现IDeletionAudited接口,如下所示:
public interface IDeletionAudited : ISoftDelete { long? DeleterUserId { get; set; } DateTime? DeletionTime { get; set; } }
IDeletionAudited扩展了ISoftDelete接口。当实体被删除时,ABP自动记录这些属性。
如果想让实体实现所有的审计接口(creation、modification和deletion),可以直接实现IFullAudited接口,因为它继承自所有接口:
public interface IFullAudited : IAudited, IDeletionAudited { }
有个捷径,可以使实体继承自FullAuditedEntity类,它实现了所有。
- NOTE1:所有的审计接口和类都有一个泛型版本,用来定义User实体(如ICreationAudited<TUser>和FullAuditedEntity<TPrimaryKey,TUser>)的导航属性。
- NOTE2:所有这些都有一个聚合根版本,如AuditedAggregateRoot。
一些实体需要被记录为激活或失活。然后可以基于实体的激活/失活状态进行操作。可以实现IPassiveable接口,这个接口便是为这个目的设计的。它定义了IsActive属性。
如果实体第一次创建时为激活状态,那么可以在构造函数中设置IsActive为true。
这和软删除(IsDeleted)是不同的。如果实体被软删除,它不能从数据库中获取(ABP默认会阻止获取),但是,对于激活/失活的实体,控制获取实体完全由自己决定。
当实体被插入、更新或删除时,ABP自动触发这些确定的事件。然后,我们可以注册这些事件,执行需要的任何逻辑。参见在事件总线文档的预定义事件部分了解更多信息。
实际上,Entity类实现了IEntity接口(Entity<TPrimaryKey>实现了IEntity<TPrimaryKey>)。如果不想从Entity类集成,可以直接实现这些接口。其他实体类也有相关的接口。但是,不建议使用这种方式,除非有好的理由不从Entity类继承。