ABP官方文档(十九)【工作单元】

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)]。这样即明确而且可读性也更好。

注意,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);
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值