3.5 ABP领域层 - 工作单元
3.5.1 简介
连接和事务管理是使用数据库的应用程序最重要的概念之一。当你开启一个数据库连接,什么时候开始事务,如何释放连接;诸如此类的…。ABP默认使用 工作单元 来管理数据库连接和事务。
3.5.2 在ABP中管理连接和事务
当 进入 某个 事务单元 的时候,ABP 会 打开 数据库的连接,并开始 事务 操作(它可能不会立即打开,但是会在首次使用数据库的时候打开)。所以你可以安全在这个方法中使用连接。在该方法的最后,该事务会被 提交,且该连接会被 释放。如果该方法抛出任何异常,事务会 回滚 且连接会被释放。通过这种方式,工作单元方法是 原子性的。ABP会自动的执行这些操作。
如果工作单元方法调用其它工作单元的方法,它们使用相同的连接和事务。第一个进入的方法管理连接和事务,其它只是使用它。
1. 约定的工作单元方法
下面这些方法默认是工作单元的:
- 所有的 MVC,Web API 以及 ASP.NET Core MVC 控制器的Actions
- 所有的 Application Service 方法
- 所有的 Repository 方法
假设我们有个Application Service方法,如下所示:
public class PersonAppService : IPersonAppService
{
private readonly IPersonRepository _personRepository;
private readonly IStatisticsRepository _statisticsRepository;
public PersonAppService(IPersonRepository personRepository, IStatisticsRepository statisticsRepository)
{
_personRepository = personRepository;
_statisticsRepository = statisticsRepository;
}
public void CreatePerson(CreatePersonInput input)
{
var person = new Person { Name = input.Name, EmailAddress = input.EmailAddress };
_personRepository.Insert(person);
_statisticsRepository.IncrementPeopleCount();
}
}
在CreatePerson方法中,我们使用 person 仓储新增一个 person,并且使用 statistics 仓储增加总 people 数量。这两个仓储 共享 同一个连接和事务。在这个例子中,因为这是一个应用服务的方法,所以工作单元是默认开启的。当进入到 CreationPerson 这个方法的时候,ABP会打开一个数据库连接,并且开启一个事务,若没有任何异常抛出,接着在该事务单元的末尾提交这个事务;若有异常被抛出,则会回滚这个事务。在这种机制下,所有数据库的操作在 CreatePerson 中,都是 原子性的(工作单元)。
除了默认约定的工作单元类,你也可以在模块的 PreInitialize 方法中,添加你自己约定的工作单元类。如下所示:
Configuration.UnitOfWork.ConventionalUowSelectors.Add(type => ...);
如果该类型是一个约定的工作单元类,你应该检查该类型并且返回true。
2. 控制工作单元
上面所定义的方法,工作单元会 隐式的 工作。在大多数情况下,对于web应用,你没必要手动的控制工作单元。如果你在某些地方想控制工作单元,你可以 显式的 使用它。这里有两种方法来使用它:
UnitOfWork特性
首先,首选的方式是使用 UnitOfWork特性 特性,例如:
[UnitOfWork]
public void CreatePerson(CreatePersonInput input)
{
var person = new Person { Name = input.Name, EmailAddress = input.EmailAddress };
_personRepository.Insert(person);
_statisticsRepository.IncrementPeopleCount();
}
因此,CreatePerson会以工作单元的形式来管理数据库连接和事务,两个仓储使用同一个工作单元。
注意:如果这是一个 Application Service 方法,那么不需要使用 UnitOfWork 特性。详细了解请查询下面的 对于工作单元特性的限制。
对于工作单元特性的某些选项,可以查阅 工作单元详情章节
IUnitOfWorkManager
第二种方式是使用 IUnitOfWorkManager.Begin(…) 方法,如下所示:
public class MyService
{
private readonly IUnitOfWorkManager _unitOfWorkManager;
private readonly IPersonRepository _personRepository;
private readonly IStatisticsRepository _statisticsRepository;
public MyService(IUnitOfWorkManager unitOfWorkManager, IPersonRepository personRepository, IStatisticsRepository statisticsRepository)
{
_unitOfWorkManager = unitOfWorkManager;
_personRepository = personRepository;
_statisticsRepository = statisticsRepository;
}
public void CreatePerson(CreatePersonInput input)
{
var person = new Person { Name = input.Name, EmailAddress = input.EmailAddress };
using (var unitOfWork = _unitOfWorkManager.Begin())
{
_personRepository.Insert(person);
_statisticsRepository.IncrementPeopleCount();
unitOfWork.Complete();
}
}
}
如上所示,你可以注入并且使用IUnitOfWorkManager(某些基类默认已经实现了 UnitOfWorkManager 的注入:MVC的控制器,Application Service,Domain Service 等等)。因此,你可以对工作单元创建更多的限制性作用域。以这种方式,你可以手动调用 Complete 方法。如果你不调用,事务会回滚并且所有更改都不会被保存。
ABP对begin方法有很多的重载,可以用来设置 工作单元的选项。如果没有其它更好的意图,那么最好是使用 UnitOfWork 特性,因为它更方便。
3.5.3 工作单元详情
1. 禁用工作单元
你或许会想要禁用工作单元 为那些约定的工作单元方法 (例如:应用服务的方法默认是工作单元的;ApplicationService 实现了 IApplicationService) 。要想做到这个,使用UnitOfWorkAttribute的 IsDisabled 属性。示例如下:
[UnitOfWork(IsDisabled = true)]
public virtual void RemoveFriendship(RemoveFriendInput input)
{
_friendshipRepository.Delete(input.Id);
}
通常你不需要这么做,但在有些情况下,你或许会想要禁用工作单元:
- 你可能想在限制性的作用域中使用UnitOfWork,我们可以使用 UnitOfWork 特性中的 Scope 参数 来设置,该参数类型是 TransactionScopeOption。
注意,如果工作单元方法调用这个RemoveFriendship方法,这个方法是禁用且忽略的,并且它和调用它的方法使用同一个工作单元。因此,要谨慎的使用禁用功能。同样地,上述代码能很好的工作,因为仓储方法是默认为工作单元的。
2. 非事务性工作单元
工作单元默认是事务性的(这是它的天性)。因此,ABP 启动/提交/回滚 一个显式的数据库级事务。在有些特殊案例中,事务可能会导致一些问题,因为它可能会锁住数据库中的某些数据行或是数据表。在此这些情况下, 你或许会想要禁用数据库级事务。我们可以使用 UnitOfWork 特性来设置它构造函数中的 isTransactional 参数来配置一个非事务的操作。示例如下:
[UnitOfWork(isTransactional: false)]
public GetTasksOutput GetTasks(GetTasksInput input)
{
var tasks = _taskRepository.GetAllWithPeople(input.AssignedPersonId, input.State);
return new GetTasksOutput
{
Tasks = Mapper.Map<List<TaskDto>>(tasks)
};
}
我建议这样使用这个特性 [UnitOfWork(isTransaction:false)]。这样即明确而且可读性也更好。
注意,ORM框架(像NHibernate和EntityFramework)会使用单一命令来内部的保存更改。假设你在一个非事务性的工作单元中更新了少数实体数据。即使在这个情况下:用一个单一数据库命令在工作单元的末尾执行所有的更新。但是,如果在一个非事务的工作单元中你直接执行SQL查询,它会立即被执行且不会回滚。
这里有一个非事务性UoW的限制。如果你已经在事务性的工作单元作用域内,即使你设定 isTransactional 为 false 这个操作也会被忽略(在一个事务单元中,使用 TransactionScopeOption 来创建一个非事务的工作单元)。
应该谨慎的使用非事务性工作单元,因为在大多数的情况下,需要事务来维护事务的完整性。如果你的方法只是读取数据,不改变数据,那么当然可以采用非事务性的。
3. 工作单元调用其它工作单元
工作单元是可嵌套的,如果某个工作单元的方法,调用其它工作单元的方法,它们共享相同的连接和事务。第一个方法管理连接,而其它则使用该连接。
4. 工作单元作用域
你可以在其它事务中创建一个不同于该事务的隔离的事务,或者在该事务中创建一个非事务作用域。.NET 定义了 TransactionScopeOption 类来实现这样的做法。你可以设置工作单元的作用域选项来控制它。
5. 自动保存更改
如果某个方法是工作单元的,ABP会在方法执行的末尾自动保存所有的更改,假设我们需要一个更新person名字的方法:
[UnitOfWork]
public void UpdateName(UpdateNameInput input)
{
var person = _personRepository.Get(input.PersonId);
person.Name = input.NewName;
}
这样,名字就被修改了!我们甚至没有调用_personRepository.Update方法。ORM框架会在工作单元内持续追踪实体所有的变化,且反映所有变化到数据库中。
注意:我们可以不在方法上加上 UnitOfWork 特性,如果该方法是约定的工作单元方法。
6. IRepository.GetAll() 方法
当你在仓储外调用GetAll方法方法时,数据库的连接必须是开启的,因为它返回IQueryable类型的对象。这是必须的,因为IQueryable对象是延迟执行的,它并不会马上执行数据库查询,直到你调用ToList()方法或在foreach循环中使用IQueryable(或以某种方式访问查询项时)。因此,当你调用ToList()方法,数据库连接必需是启用状态。
请看下面示例:
[UnitOfWork]
public SearchPeopleOutput SearchPeople(SearchPeopleInput input)
{
//取得 IQueryable<Person>
var query = _personRepository.GetAll();
//若有选取,则添加一些过滤条件
if(!string.IsNullOrEmpty(input.SearchedName)) {
query = query.Where(person => person.Name.StartsWith(input.SearchedName));
}
if(input.IsActive.HasValue) {
query = query.Where(person => person.IsActive == input.IsActive.Value);
}
//取得分页结果集
var people = query.Skip(input.SkipCount).Take(input.MaxResultCount).ToList();
return new SearchPeopleOutput { People = Mapper.Map<List<PersonDto>>(people) };
}
在这里,SearchPeople方法必需是工作单元,因为IQueryable的ToList()方法在方法体内调用,在执行IQueryable.ToList()方法的时候,数据库连接必须开启。
在大多数情况下,web应用需要你安全的使用GetAll方法,因为所有控制器的Action默认是工作单元的,因此在整个连接中,数据库连接必须是有效连接的。
7. 对于工作单元特性的限制
你可以使用 UnitOfWork 特性 :
凡是实现了这些接口(如:应用服务实现了服务接口, ApplicationService实现了IAppliationService)的类的方法是 :public 或者 public virtual。
所有的自注入类的 public 或者 public virtual (例如:MVC 控制器或者 Web API 控制器)。
所有 protected virtual 方法。
建议使用 virtual 方法。你无法将工作单元应用在 private方法上。因为,ABP使用dynamic proxy来实现,而私有方法就无法使用继承的方法来实现。当你不使用依赖注入且自行实例化该类,那么UnitOfWork特性(以及任何代理)就无法正常运作。
3.5.5 选项
有一些选项可以用来改变工作单元的行为。
首先,我们可以在启动配置中改变所有工作单元的默认值。这通常是用了模块中的PreInitialize方法来实现。
public class SimpleTaskSystemCoreModule : AbpModule
{
public override void PreInitialize() {
Configuration.UnitOfWork.IsolationLevel = IsolationLevel.ReadCommitted;
Configuration.UnitOfWork.Timeout = TimeSpan.FromMinutes(30);
}
//...其它模块方法
}
其次,我们可以覆写某个特定工作单元的默认值。例如:使用 UnitOfWork特性的构造函数或者 IUnitOfWorkManager.Begin 方法。
最后,你可以为ASP.NET MVC,Web API 以及 ASP.NET Core MVC 控制器使用启动配置来配置工作单元。
3.5.6 方法
工作单元是无缝工作且不可视的。但是,在有些特例下,你需要调用它的方法。
你可以在下面两个方式中选取一个来访问当前工作单元:
- 如果你的类是派生自某些特定的基类(ApplicationService, DomainService, AbpController, AbpApiController 等等),那么你可以直接使用 CurrentUnitOfWork 属性。
- 你可以在任意类中注入 IUnitOfWorkManager 接口,并使用 IUnitOfWorkManager.Current 属性。
SaveChanges
在工作单元结束时ABP会保存所有的更改,而你不需要做任何事情。但是有些时候,你或许会想要在工作单元的执行过程中就保存更改。例如:使用EntityFramerok保存一个新实体的时候取得一个这个实体的ID。
你可以使用当前工作单元的 SaveChanges 或者 SaveChangesAsync 方法。
注意:如果当前工作单元是事务性的,并且有异常发生,所有在事务中的更改会回滚,甚至是保存的更改。
3.5.7 事件
工作单元具有 Completed,Failed 以及 Disposed 事件。你可以注册这些事件并且执行所需的操作。例如:当前工作单元成功执行完成,你可能想运行某些代码:
public void CreateTask(CreateTaskInput input)
{
var task = new Task { Description = input.Description };
if (input.AssignedPersonId.HasValue)
{
task.AssignedPersonId = input.AssignedPersonId.Value;
_unitOfWorkManager.Current.Completed += (sender, args) => { /* TODO: 发送邮件给已分配的人 */
};
}
_taskRepository.Insert(task);
}