ABP框架 - 工作单元

文档目录

 

本节内容:

 

简介

在一个使用数据库的应用里,连接和事务是非常重要的,何时打开一个连接,何时开启一个事务,如果释放连接等等。ABP通过工作单元系统管理数据库的连接和事务。

 

在ABP中管理连接和事务

当进行一个工作单元方法,ABP打开一个数据库连接(可能不是马上打开,在第一次用到数据库时打开,这要看ORM供应器的实现了)和开始一个事务,所以你可以安全地在这个方法内使用连接,在这个方法的最后,提交事务并释放连接,如果这个方法抛出任何的异常,回滚事务并释放连接,这种方式,一个工作单元就是原子性的。ABP自动完成这些操作。

如果一个工作单元方法调用另一个工作单元方法,两个会使用同一个连接和事务,第一个方法管理连接和事务,另一个使用它们。

 

约定的工作单元方法

有些方法默认为工作单元方法:

假设我们有一个应用服务方法,如下所示:

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方法里,我们使用人员仓储插入一个人员,并使用统计仓储递增人员总数,这两个仓储共享相同的连接和事务,因为应用服务方法默认为一个工作单元。当进入CreatePerson方法时,ABP打开一个连接并开始一个事务,在方法结束时,如果没有异常出现,就提交事务并释放连接,这种方式,所有数据库操作,在CreatePerson方法里都变成是原子性的。

 

控制工作单元

上面的方法里,工作单元是暗中工作的。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方法成为一个工作单元,管理数据库连接和事务,两个仓储使用相同的工作单元,注意,如果在一个应用服务方法里,不需要使用UnitOfWork特性。 请参见”UnitOfWork 特性限制“小节。

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控制器、应用服务领域服务等),因此,你可以创建更多有域限制的工作单元。用这种方法,你应该手动调用Complete方法,如果你不调用,事务会回滚,修改不会被保存。

Begin方法有几个设置工作单元选项的重载,如果没有好的理由,最好使用UnitOfWork特性。

 

工作单元详情

禁用工作单元

你如果想为约定的工作单元方法禁用工作单元,使用UnitOfWork特性的IsDisabled属性。例如:

[UnitOfWork(IsDisabled = true)]
public virtual void RemoveFriendship(RemoveFriendshipInput input)
{
    _friendshipRepository.Delete(input.Id);
}

通常,你不会想这么做,但在有些情况下你需要禁用工作单元:

  • 你可能想限制工作单元的域,如同上面所述,就使用UnitOfWorkScope。

注意:如果一个工作单元方法调用这个RemoveFriendship方法,禁用特性会被忽略,该方法使用与调用者方法相同的工作单元。所以小心使用禁用特性。同样地,上面的代码也会工作得很好,因为仓储方法默认是个工作单元。

 

非事务性工作单元

一个工作单元默认就是事务性的,因此ABP开始/提交/回滚数据库级别事务。在一些特殊情况下,事务可能会引起问题,因为它可能锁住数据库的一些行或表。这种情况下,你可能想禁用数据库级别事务。UnitOfWork特性可以在它的构造器里,获得一个boolean值作为非事务性标识。使用示例:

[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(isTransactional:false)]这样使用这个特性,我认为这样更可读更明确。但是你也可以[UnitOfWork(false)]这样用。

注意:ORM框架(如NHibernate和EntityFramework)内部使用一个单独的命令保存修改。假设你在一个非事务工作单元里更新一些实体,即使是这种况下,所有更新也是在工作单元的最后,执行一个单独的数据库命令。但是,如果在一个非事务性工作单元里,直接执行一条Sql语句,它会被直接执行并且不会被回滚。

非事务性工作单元有一个限制,如果你已经在一个事务性工作单元域内,把isTransactional设置false也会被忽略(在一个事务性工作单元里,使用事务域选项创建一个非事务性工作单元)。

使用非事务性工作单元要小心,因为大部分情况下,应当用事务性保证数据完整性。如果你只是读取数据,而不做修改,那么就可以安全地使用非事务性。

 

一个工作单元方法调用另一个方法

工作单元是环绕的,如果一个工作单元方法调用另一个工作单元方法,他们共享相同的连接和事务,第一个方法管理连接和事务,另一个方法使用它们。

 

工作单元域

你可以在一个事务里创建另一个独立的事务,或在一个事务里创建一个非事务性的域,.NET为它定义了TransactionScopeOption。你可以通过设置工作单元域选项来控制它。

 

自动保存修改

如果一个方法是工作单元,ABP会自动在这个方法的最后,保存所有修改。假设我们需要一个方法更新person的name:

[UnitOfWork]
public void UpdateName(UpdateNameInput input)
{
    var person = _personRepository.Get(input.PersonId);
    person.Name = input.NewName;
}

这就是所有代码,name会被修改!我们甚至不用调用_personRepository.Update方法。在一个工作单元里,ORM框架跟踪实体的所有修改,并反射到数据库。

注意:不需要为约定的工作单元方法声明UnitOfWork。

 

IRepository.GetAll() 方法

当你在一个仓储方法外调用GetAll(),它必须要有一个打开的数据库连接,因为它只是返回IQueryable。这是必须的,因为IQuery的延迟执行。它没有执行数据库查询,除非你调用ToList()或在一个foreach循环里使用IQueryable(或以某种方式访问查询里的项),所以当你调用ToList()方法时,数据库连接必须可用。

考虑如下示例:

[UnitOfWork]
public SearchPeopleOutput SearchPeople(SearchPeopleInput input)
{
    //Get IQueryable<Person>
    var query = _personRepository.GetAll();

    //Add some filters if selected
    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);
    }

    //Get paged result list
    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默认都是工作单元并且数据库连接在整个请求里都是可用的。

 

UnitOfWrok 特性限制

可以使用UnitOfWork为:

  • 类的所有public或public virtual方法并且在接口之上应用了这个特性(如一个应用服务就是在服务接口上使用了这个特征)。
  • 类的所有public virtual并自注入的方法(如Mvc控制器和Web Api控制器)。
  • 所有protected virtual方法。

建议一直使用virtual方法,你可以不为private方法这么做,因为ABP为它们使用动态代理,而private方法对于继承它的类来说是看不见的。如果不使用依赖注入并自己实例化这个类,UnitOfWork特性(和任何代理)都无法工作。

 

选项

有些选项可以改变一个工作单元的行为。

首先,我们可以在启动配置里修改所有工作单元的默认值,这通常在我们模块的预初始化方法里。

public class SimpleTaskSystemCoreModule : AbpModule
{
    public override void PreInitialize()
    {
        Configuration.UnitOfWork.IsolationLevel = IsolationLevel.ReadCommitted;
        Configuration.UnitOfWork.Timeout = TimeSpan.FromMinutes(30);
    }

    //...other module methods
}

其次,我们可以为一个特殊工作单元重写默认值,为此,UnitOfWork特性构造器和IUnitOfWorkManager.Begin方法有获取选项的重载。

最后,你可以通过启动配置,为Asp.net Mvc、Web Api和Asp.net Core Mvc控制器配置工作单元特性默认值(查看它们各自的文档)。

 

方法

UnitOfWork系统悄无声息、无缝地工作着,但是在有些特殊情况下,你需要调用它们的方法。

你可以用如下作一种方式,访问当前工作单元:

  • 如果你的类是从有些特殊基类(ApplicationService、AbpController、AbpApiController等)继承而来,可以直接使用CurrentUnitOfWork属性。
  • 你可以把IunitOfWorkManager注入到任何类里,然后使用IUnitOfWorkManager.Current属性。

 

SaveChanges

ABP在一个工作单元的最后,保存所有修改,你不必做任何事,但是有时你想在一个工作单元操作的中间保存修改到数据库,例如在EntityFramework里,为获取新实体的Id,先进行保存。

你可以使用当前工作单元的SaveChanges或SaveChangesAsync方法。

注意:如果当前工作单元是事务性的,所有的修改在出现异常情况下回滚,不然保存。

 

事件

一个工作单元有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: Send email to assigned person */ };
    }

    _taskRepository.Insert(task);
}

 

kid1412附:英文原文:http://www.aspnetboilerplate.com/Pages/Documents/Unit-Of-Work

posted @ 2016-10-26 01:40 kid1412 阅读(...) 评论(...) 编辑 收藏