解析ABP框架领域层中的实体类与仓储类_基础应用

领域层
实体是DDD(领域驱动设计)的核心概念之一。Eric Evans是这样描述的“很多对象不是通过它们的属性定义的,而是通过一连串的连续性事件和标识定义的”(引用领域驱动设计一书)。

译者注:对象不是通过它们的属性来下根本性的定义,而应该是通过它的线性连续性和标识性定义的。。所以,实体是具有唯一标识的ID且存储在数据库中。实体通常被映射成数据库中的一个表。

实体类(Entity classes)
在ABP中,实体继承自Entity类,请看下面示例:

public class Person : Entity
{
  public virtual string Name { get; set; }

  public virtual DateTime CreationTime { get; set; }

  public Task()
  {
    CreationTime = DateTime.Now;
  }
}

Person 类被定义为一个实体。它具有两个属性,它的父类中有Id属性。Id是该实体的主键。所以,Id是所有继承自Entity类的实体的主键(所有实体的主键都是Id字段)。

Id(主键)数据类型可以被更改。默认是int(int32)类型。如果你想给Id定义其它类型,你应该像下面示例一样来声明Id的类型。

public class Person : Entity<long>
{
  public virtual string Name { get; set; }

  public virtual DateTime CreationTime { get; set; }

  public Task()
  {
    CreationTime = DateTime.Now;
  }
}

你可以设置为string,Guid或者其它数据类型。

实体类重写了 equality (==) 操作符用来判断两个实体对象是否相等(两个实体的Id是否相等)。还定义了一个IsTransient()方法来检测实体是否有Id属性。

接口约定
在很多应用程序中,很多实体具有像CreationTime的属性(数据库表也有该字段)用来指示该实体是什么时候被创建的。APB提供了一些有用的接口来实现这些类似的功能。也就是说,为这些实现了这些接口的实体,提供了一个通用的编码方式(通俗的说只要实现指定的接口就能实现指定的功能)。

(1)审计(Auditing)

实体类实现 IHasCreationTime 接口就可以具有CreationTime的属性。当该实体被插入到数据库时, ABP会自动设置该属性的值为当前时间。

public interface IHasCreationTime
{
  DateTime CreationTime { get; set; }
}

Person类可以被重写像下面示例一样实现IHasCreationTime 接口:

public class Person : Entity<long>, IHasCreationTime
{
  public virtual string Name { get; set; }

  public virtual DateTime CreationTime { get; set; }

  public Task()
  {
    CreationTime = DateTime.Now;
  }
}

ICreationAudited 扩展自 IHasCreationTime 并且该接口具有属性 CreatorUserId :

public interface ICreationAudited : IHasCreationTime
{
  long? CreatorUserId { get; set; }
}

当保存一个新的实体时,ABP会自动设置CreatorUserId 的属性值为当前用户的Id

你可以轻松的实现ICreationAudited接口,通过派生自实体类 CreationAuditedEntity (因为该类已经实现了ICreationAudited接口,我们可以直接继承CreationAuditedEntity 类就实现了上述功能)。它有一个实现不同ID数据类型的泛型版本(默认是int),可以为ID(Entity类中的ID)赋予不同的数据类型。
下面是一个为实现类似修改功能的接口

public interface IModificationAudited
{
  DateTime? LastModificationTime { get; set; }
  long? LastModifierUserId { get; set; }
}

当更新一个实体时,ABP会自动设置这些属性的值。你只需要在你的实体类里面实现这些属性。

如果你想实现所有的审计属性,你可以直接扩展 IAudited 接口;示例如下:

public interface IAudited : ICreationAudited, IModificationAudited
{

}

作为一个快速开发方式,你可以直接派生自AuditedEntity 类,不需要再去实现IAudited接口(AuditedEntity 类已经实现了该功能,直接继承该类就可以实现上述功能),AuditedEntity 类有一个实现不同ID数据类型的泛型版本(默认是int),可以为ID(Entity类中的ID)赋予不同的数据类型。

(2)软删除(Soft delete)

软删除是一个通用的模式被用来标记一个已经被删除的实体,而不是实际从数据库中删除记录。例如:你可能不想从数据库中硬删除一条用户记录,因为它被许多其它的表所关联。为了实现软删除的目的我们可以实现该接口 ISoftDelete:

public interface ISoftDelete{
  bool IsDeleted { get; set; }
}

ABP实现了开箱即用的软删除模式。当一个实现了软删除的实体正在被被删除,ABP会察觉到这个动作,并且阻止其删除,设置IsDeleted 属性值为true并且更新数据库中的实体。也就是说,被软删除的记录不可以从数据库中检索出,ABP会为我们自动过滤软删除的记录。(例如:Select查询,这里指通过ABP查询,不是通过数据库中的查询分析器查询。)

如果你用了软删除,你有可能也想实现这个功能,就是记录谁删除了这个实体。要实现该功能你可以实现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 类派生你的实体类,因为该类已经实现了IFullAudited接口。

注意:所有的审计接口和类都有一个泛型模板为了导航定义属性到你的User 实体(例如:ICreationAudited<TUser>和FullAuditedEntity<TPrimaryKey, TUser>),这里的TUser指的进行创建,修改和删除的用户的实体类的类型,详细请看源代码(Abp.Domain.Entities.Auditing空间下的FullAuditedEntity<TPrimaryKey, TUser>类),TprimaryKey 只的是Entity基类Id类型,默认是int。

(3)激活状态/闲置状态(Active/Passive)

有些实体需要被标记为激活状态或者闲置状态。那么你可以为实体采取active/passive状态的行动。基于这个原因而创建的实体,你可以扩展IPassivable 接口来实现该功能。该接口定义了IsActive 的属性。

如果你首次创建的实体被标记为激活状态,你可以在构造函数设置IsActive属性值为true。

这是不同于软删除(IsDeleted)。如果实体被软删除,它不能从数据库中被检索到(ABP已经过滤了软删除记录)。但是对于激活状态/闲置状态的实体,你完全取决于你怎样去获取这些被标记了的实体。

IEntity接口
事实上Entity 实现了IEntity 接口(和Entity<TPrimaryKey> 实现了 IEntity<TPrimaryKey>接口)。如果你不想从Entity 类派生,你能直接的实现这些接口。其他实体类也可以实现相应的接口。但是不建议你用这种方式。除非你有一个很好的理由不从Entity 类派生。

仓储(Repositories)
仓储定义:“在领域层和数据映射层的中介,使用类似集合的接口来存取领域对象”(Martin Fowler)。

实际上,仓储被用于领域对象在数据库上的操作(实体Entity和值对象Value types)。一般来说,我们针对不同的实体(或聚合根Aggregate Root)会创建相对应的仓储。

IRepository接口
在ABP中,仓储类要实现IRepository接口。最好的方式是针对不同仓储对象定义各自不同的接口。

针对Person实体的仓储接口声明的示例如下所示:

public interface IPersonRepository : IRepository<Person>
{
}

IPersonRepository继承自IRepository<TEntity>,用来定义Id的类型为int(Int32)的实体。如果你的实体Id数据类型不是int,你可以继承IRepository<TEntity, TPrimaryKey>接口,如下所示:

public interface IPersonRepository : IRepository<Person, long>
{
}

对于仓储类,IRepository定义了许多泛型的方法。比如: Select,Insert,Update,Delete方法(CRUD操作)。在大多数的时候,这些方法已足已应付一般实体的需要。如果这些方对于实体来说已足够,我们便不需要再去创建这个实体所需的仓储接口/类。在Implementation章节有更多细节。

(1)查询(Query)

IRepository定义了从数据库中检索实体的常用方法。

A、取得单一实体(Getting single entity):

TEntity Get(TPrimaryKey id);
Task<TEntity> GetAsync(TPrimaryKey id);
TEntity Single(Expression<Func<TEntity, bool>> predicate);
TEntity FirstOrDefault(TPrimaryKey id);
Task<TEntity> FirstOrDefaultAsync(TPrimaryKey id);
TEntity FirstOrDefault(Expression<Func<TEntity, bool>> predicate);
Task<TEntity> FirstOrDefaultAsync(Expression<Func<TEntity, bool>> predicate);
TEntity Load(TPrimaryKey id);

Get方法被用于根据主键值(Id)取得对应的实体。当数据库中根据主键值找不到相符合的实体时,它会抛出例外。Single方法类似Get方法,但是它的输入参数是一个表达式而不是主键值(Id)。因此,我们可以写Lambda表达式来取得实体。示例如下:

var person = _personRepository.Get(42);
var person = _personRepository.Single(p => o.Name == "Halil ibrahim Kalkan");

注意,Single方法会在给出的条件找不到实体或符合的实体超过一个以上时,都会抛出例外。

FirstOrDefault也一样,但是当没有符合Lambda表达式或Id的实体时,会回传null(取代抛出异常)。当有超过一个以上的实体符合条件,它只会返回第一个实体。

Load并不会从数据库中检索实体,但它会创建延迟执行所需的代理对象。如果你只使用Id属性,实际上并不会检索实体,它只有在你存取想要查询实体的某个属性时才会从数据库中查询实体。当有性能需求的时候,这个方法可以用来替代Get方法。Load方法在NHibernate与ABP的整合中也有实现。如果ORM提供者(Provider)没有实现这个方法,Load方法运行的会和Get方法一样。

ABP有些方法具有异步(Async)版本,可以应用在异步开发模型上(见Async方法相关章节)。

B、取得实体列表(Getting list of entities):

List<TEntity> GetAllList();
Task<List<TEntity>> GetAllListAsync();
List<TEntity> GetAllList(Expression<Func<TEntity, bool>> predicate);
Task<List<TEntity>> GetAllListAsync(Expression<Func<TEntity, bool>> predicate);
IQueryable<TEntity> GetAll();

GetAllList被用于从数据库中检索所有实体。重载并且提供过滤实体的功能,如下:

var allPeople = _personRespository.GetAllList();
var somePeople = _personRepository.GetAllList(person => person.IsActive && person.Age > 42);

GetAll返回IQueryable<T>类型的对象。因此我们可以在调用完这个方法之后进行Linq操作。示例:

//例子一
var query = from person in _personRepository.GetAll()
where person.IsActive
orderby person.Name
select person;
var people = query.ToList();
//例子二
List<Person> personList2 = _personRepository.GetAll().Where(p => p.Name.Contains("H")).OrderBy(p => p.Name).Skip(40).Take(20).ToList();

 如果调用GetAll方法,那么几乎所有查询都可以使用Linq完成。甚至可以用它来编写Join表达式。

说明:关于IQueryable<T>
当你调用GetAll这个方法在Repository对象以外的地方,必定会开启数据库连接。这是因为IQueryable<T>允许延迟执行。它会直到你调用ToList方法或在forEach循环上(或是一些存取已查询的对象方法)使用IQueryable<T>时,才会实际执行数据库的查询。因此,当你调用ToList方法时,数据库连接必需是启用状态。我们可以使用ABP所提供的UnitOfWork特性在调用的方法上来实现。注意,Application Service方法预设都已经是UnitOfWork。因此,使用了GetAll方法就不需要如同Application Service的方法上添加UnitOfWork特性。

有些方法拥有异步版本,可应用在异步开发模型(见关于async方法章节)。

自定义返回值(Custom return value)

ABP也有一个额外的方法来实现IQueryable<T>的延迟加载效果,而不需要在调用的方法上添加UnitOfWork这个属性卷标。

T Query<T>(Func<IQueryable<Tentity>,T> queryMethod);

查询方法接受Lambda(或一个方法)来接收IQueryable<T>并且返回任何对象类型。示例如下:

var  people = _personRepository.Query(q => q.Where(p => p.Name.Contains("H")).OrderBy(p => p.Name).ToList());
因为是采用Lambda(或方法)在仓储对象的方法中执行,它会在数据库连接开启之后才被执行。你可以返回实体集合,或一个实体,或一个具部份字段(注: 非Select *)或其它执行查询后的查询结果集。

(2)新增(insert)

IRepository接口定义了简单的方法来提供新增一个实体到数据库:

TEntity Insert(TEntity entity);
Task<TEntity> InsertAsync(TEntity entity);
TPrimaryKey InsertAndGetId(TEntity entity);
Task<TPrimaryKey> InsertAndGetIdAsync(TEntity entity);
TEntity InsertOrUpdate(TEntity entity);
Task<TEntity> InsertOrUpdateAsync(TEntity entity);
TPrimaryKey InsertOrUpdateAndGetId(TEntity entity);
Task<TPrimaryKey> InsertOrUpdateAndGetIdAsync(TEntity entity);

新增方法会新增实体到数据库并且返回相同的已新增实体。InsertAndGetId方法返回新增实体的标识符(Id)。当我们采用自动递增标识符值且需要取得实体的新产生标识符值时非常好用。InsertOfUpdate会新增或更新实体,选择那一种是根据Id是否有值来决定。最后,InsertOrUpdatedAndGetId会在实体被新增或更新后返回Id值。

所有的方法都拥有异步版本可应用在异步开发模型(见关于异步方法章节)

(3)更新(UPDATE)

IRepository定义一个方法来实现更新一个已存在于数据库中的实体。它更新实体并返回相同的实体对象。

TEntity Update(TEntity entity);
Task<TEntity> UpdateAsync(TEntity entity);

(4)删除(Delete)

IRepository定了一些方法来删除已存在数据库中实体。

void Delete(TEntity entity);
Task DeleteAsync(TEntity entity);
void Delete(TPrimaryKey id);
Task DeleteAsync(TPrimaryKey id);
void Delete(Expression<Func<TEntity, bool>> predicate);
Task DeleteAsync(Expression<Func<TEntity, bool>> predicate);

第一个方法接受一个现存的实体,第二个方法接受现存实体的Id。

最后一个方法接受一个条件来删除符合条件的实体。要注意,所有符合predicate表达式的实体会先被检索而后删除。因此,使用上要很小心,这是有可能造成许多问题,假如果有太多实体符合条件。

所有的方法都拥有async版本来应用在异步开发模型(见关于异步方法章节)。

(5)其它方法(others)

IRepository也提供一些方法来取得数据表中实体的数量。

int Count();
Task<int> CountAsync();
int Count(Expression<Func<TEntity, bool>> predicate);
Task<int> CountAsync(Expression<Func<TEntity, bool>> predicate);
Long LongCount();
Task<long> LongCountAsync();
Long LongCount(Expression<Func<TEntity, bool>> predicate);
Task<long> LongCountAsync(Expression<TEntity, bool>> predicate);

 所有的方法都拥有async版本被应用在异步开发模型(见关于异步方法章节)。

(6)关于异步方法(About Async methods)

ABP支持异步开发模型。因此,仓储方法拥有Async版本。在这里有一个使用异步模型的application service方法的示例:

public class PersonAppService : AbpWpfDemoAppServiceBase, IPersonAppService
{
  private readonly IRepository<Person> _personRepository;

  public PersonAppService(IRepository<Person> personRepository)
  {
    _personRepository = personRepository;
  }

  public async Task<GetPeopleOutput> GetAllPeople()
  {
    var people = await _personRepository.GetAllListAsync();

    return new GetPeopleOutput
    {
      People = Mapper.Map<List<PersonDto>>(people)
    };
  }
}

GetAllPeople方法是异步的并且使用GetAllListAsync与await保留关键字。

Async不是在每个ORM框架都有提供。

上例是从EF所提供的异步能力。如果ORM框架没有提供Async的仓储方法则它会以同步的方式操作。同样地,举例来说,InsertAsync操作起来和EF的新增是一样的,因为EF会直到单元作业(unit of work)完成之后才会写入新实体到数据库中(DbContext.SaveChanges)。

仓储的实现
ABP在设计上是采取不指定特定ORM框架或其它存取数据库技术的方式。只要实现IRepository接口,任何框架都可以使用。

仓储要使用NHibernate或EF来实现都很简单。

EntityFramework
当你使用NHibernate或EntityFramework,如果提供的方法已足够使用,你就不需要为你的实体创建仓储对象了。我们可以直接注入IRepository<TEntity>(或IRepository<TEntity, TPrimaryKey>)。下面的示例为application service使用仓储对象来新增实体到数据库:

public class PersonAppService : IPersonAppService
{
  private readonly IRepository<Person> _personRepository;

  public PersonAppService(IRepository<Person> personRepository)
  {
    _personRepository = personRepository;
  }

  public void CreatePerson(CreatePersonInput input)
  {
    person = new Person { Name = input.Name, EmailAddress = input.EmailAddress };

    _personRepository.Insert(person);
  }
}

 PersonAppService的建构子注入了IRepository<Person>并且使用其Insert方法。当你有需要为实体创建一个客制的仓储方法,那么你就应该创建一个仓储类给指定的实体。

管理数据库连接
数据库连接的开启和关闭,在仓储方法中,ABP会自动化的进行连接管理。

当仓储方法被调用后,数据库连接会自动开启且启动事务。当仓储方法执行结束并且返回以后,所有的实体变化都会被储存, 事务被提交并且数据库连接被关闭,一切都由ABP自动化的控制。如果仓储方法抛出任何类型的异常,事务会自动地回滚并且数据连接会被关闭。上述所有操作在实现了IRepository接口的仓储类所有公开的方法中都可以被调用。

如果仓储方法调用其它仓储方法(即便是不同仓储的方法),它们共享同一个连接和事务。连接会由仓储方法调用链最上层的那个仓储方法所管理。更多关于数据库管理,详见UnitOfWork文件。

储的生命周期
所有的仓储对象都是暂时性的。这就是说,它们是在有需要的时候才会被创建。ABP大量的使用依赖注入,当仓储类需要被注入的时候,新的类实体会由注入容器会自动地创建。见相根据注入文件有更多信息。

仓储的最佳实践
对于一个T类型的实体,是可以使用IRepository<T>。但别任何情况下都创建定制化的仓储,除非我们真的很需要。预定义仓储方法已经足够应付各种案例。
假如你正创建定制的仓储(可以实现IRepository<TEntity>)
仓储类应该是无状态的。这意味着, 你不该定义仓储等级的状态对象并且仓储方法的调用也不应该影响到其它调用。    
当仓储可以使用相根据注入,尽可较少或是不相根据于其它服务。 

以上是小编为您精心准备的的内容,在的博客、问答、公众号、人物、课程等栏目也有的相关内容,欢迎继续使用右上角搜索按钮进行搜索实体类
, ABP
, 领域层
仓储类
abp 仓储、abp框架、abp框架 权限管理系统、abp框架项目实战、abp 框架作者国籍,以便于您获取更多的相关知识。

时间: 2024-09-29 10:32:10

解析ABP框架领域层中的实体类与仓储类_基础应用的相关文章

解析ABP框架中的数据传输对象与应用服务_实用技巧

数据传输对象(DTOs)数据传输对象(Data Transfer Objects)用于应用层和展现层的数据传输. 展现层传入数据传输对象(DTO)调用一个应用服务方法,接着应用服务通过领域对象执行一些特定的业务逻辑并且返回DTO给展现层.这样展现层和领域层被完全分离开了.在具有良好分层的应用程序中,展现层不会直接使用领域对象(仓库,实体). 1.数据传输对象的作用:为每个应用服务方法创建DTO看起来是一项乏味耗时的工作.但如果你正确使用它们,这将会解救你的项目.为啥呢? (1)抽象领域层 (Ab

《中国人工智能学会通讯》——11.47 领域文本中的实体链接技术

11.47 领域文本中的实体链接技术 除了通用知识库,我们还拥有很多领域知识库(如 DBLP 文献网络 .IMDb 电影网络).将领域文本中的命名实体链接到相应领域知识库,对领域文本的分析和领域知识库的扩充非常有帮助.目前主流的实体链接算法都是针对通用的维基百科或者由维基百科生成的知识库(如 YAGO),这些算法都依赖于与维基百科相关的特征,例如维基百科文章中的上下文文本信息.基于维基百科的相关度度量,以及维基百科的一些特殊结构(如排歧页面.跳转页面等).由于领域知识库并不具备这些特征,所以之前

解析ABP框架中的事务处理和工作单元_实用技巧

通用连接和事务管理方法连接和事务管理是使用数据库的应用程序最重要的概念之一.当你开启一个数据库连接,什么时候开始事务,如何释放连接...诸如此类的. 正如大家都知道的,.Net使用连接池(connection pooling).因此,创建一个连接实际上是从连接池中取得一个连接,会这么做是因为创建新连接会有成本.如果没有任何连接存在于连接池中,一个新的连接对象会被创建并且添加到连接池中.当你释放连接,它实际上是将这个连接对象送回到连接池.这并不是实际意义上的释放.这个机制是由.Net所提供的.因此

Javascript技术栈中的四种依赖注入小结_基础知识

作为面向对象编程中实现控制反转(Inversion of Control,下文称IoC)最常见的技术手段之一,依赖注入(Dependency Injection,下文称DI)可谓在OOP编程中大行其道经久不衰.比如在J2EE中,就有大名鼎鼎的执牛耳者Spring.Javascript社区中自然也不乏一些积极的尝试,广为人知的AngularJS很大程度上就是基于DI实现的.遗憾的是,作为一款缺少反射机制.不支持Annotation语法的动态语言,Javascript长期以来都没有属于自己的Spri

举例详解Python中smtplib模块处理电子邮件的使用_基础知识

在基于互联网的应用中,程序经常需要自动地发送电子邮件.如:一个网站的注册系统会在用户注册时发送一封邮件来确认注册:当用户忘记登陆密码的时候,通过邮件来取回密码.smtplib模块是python中smtp(简单邮件传输协议)的客户端实现.我们可以使用smtplib模块,轻松的发送电子邮件.下面的例子用了不到十行代码来发送电子邮件:   #coding=gbk import smtplib smtp = smtplib.SMTP() smtp.connect("smtp.yeah.net"

JavaScript的Polymer框架中dom-repeat与VM的相关操作_基础知识

各种框架都有把一个列表数据绑定到 DOM 上的功能,比如 Angular 会用 ng-repeat 来绑定.那么 Polymer 呢?其实这个级别的功能属于框架的扩展功能了,Angular 的 ng-repeat 也只是个 Directive 而已.Polymer 的 dom-repeat 也是这个级别的东西. 在 Polymer 中,一切都是 Directive 的概念.dom-module 用于定义模块,它本身也是一个 Directive.dom-repeat 也是,但它不是一个标签,而是一

JS的框架Polymer中的dom-if和is属性使用说明_基础知识

我们经常会有根据某个属性的取值来觉得模板渲染的需求,那就是最基本的分支语句.Angular 中有 ng-if 可以用,那么 Polymer 中当然也有 dom-if.其实 dom-if 是个很简单的东西,它只是作为本篇话题的接入点,我想介绍的实际上是 is 这个属性. dom-if 和之前介绍过的 dom-repeat 一样,都是通过 is 属性在 tempalte 元素上使用的,比如下面这个例子就是两个模板根据一个绑定才控件上的布尔值来决定谁该被渲染 运行 <script> var Poly

JavaScript的React框架中的JSX语法学习入门教程_基础知识

什么是JSX? 在用React写组件的时候,通常会用到JSX语法,粗看上去,像是在Javascript代码里直接写起了XML标签,实质上这只是一个语法糖,每一个XML标签都会被JSX转换工具转换成纯Javascript代码,当然你想直接使用纯Javascript代码写也是可以的,只是利用JSX,组件的结构和组件之间的关系看上去更加清晰. var MyComponent = React.createClass({/*...*/}); var myElement = <MyComponent som

Backbone.js框架中Model与Collection的使用实例_基础知识

Model关于backbone,最基础的一个东西就是model,这个东西就像是后端开发中的数据库映射那个model一样,也是数据对象的模型,并且应该是和后端的model有相同的属性(仅是需要通过前端来操作的属性). 下面就从实例来一步一步的带大家来了解backbone的model到底是什么样的一个东西. 首先定义一个html的页面: <!DOCTYPE html> <html> <head> <title>the5fire-backbone-model<