3.5 ABP领域层 - 工作单元

3.5.1 简介

连接和事务管理是使用数据库的应用程序最重要的概念之一。当你开启一个数据库连接,什么时候开始事务,如何释放连接;诸如此类的...。ABP默认使用 工作单元 来管理数据库连接和事务。

3.5.2 在ABP中管理连接和事务

进入 某个 事务单元 的时候,ABP 会 打开 数据库的连接,并开始 事务 操作(它可能不会立即打开,但是会在首次使用数据库的时候打开)。所以你可以安全在这个方法中使用连接。在该方法的最后,该事务会被 提交,且该连接会被 释放。如果该方法抛出任何异常,事务会 回滚 且连接会被释放。通过这种方式,工作单元方法是 原子性的。ABP会自动的执行这些操作。

如果工作单元方法调用其它工作单元的方法,它们使用相同的连接和事务。第一个进入的方法管理连接和事务,其它只是使用它。

1. 约定的工作单元方法

下面这些方法默认是工作单元的:

假设我们有个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)]。这样即明确而且可读性也更好。但你同样可以使用[UnitOfWork(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);
}