3.5.2 在ABP中管理连接和事务

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

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

    1. 约定的工作单元方法

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

    假设我们有个Application Service方法,如下所示:

    在CreatePerson方法中,我们使用 person 仓储新增一个 person,并且使用 statistics 仓储增加总 people 数量。这两个仓储 共享 同一个连接和事务。在这个例子中,因为这是一个应用服务的方法,所以工作单元是默认开启的。当进入到 CreationPerson 这个方法的时候,ABP会打开一个数据库连接,并且开启一个事务,若没有任何异常抛出,接着在该事务单元的末尾提交这个事务;若有异常被抛出,则会回滚这个事务。在这种机制下,所有数据库的操作在 CreatePerson 中,都是 原子性的(工作单元)。

    除了默认约定的工作单元类,你也可以在的 PreInitialize 方法中,添加你自己约定的工作单元类。如下所示:

    如果该类型是一个约定的工作单元类,你应该检查该类型并且返回true。

    2. 控制工作单元

    上面所定义的方法,工作单元会 隐式的 工作。在大多数情况下,对于web应用,你没必要手动的控制工作单元。如果你在某些地方想控制工作单元,你可以 显式的 使用它。这里有两种方法来使用它:

    UnitOfWork特性

    首先,首选的方式是使用 UnitOfWork特性 特性,例如:

    1. [UnitOfWork]
    2. public void CreatePerson(CreatePersonInput input)
    3. {
    4. _personRepository.Insert(person);
    5. _statisticsRepository.IncrementPeopleCount();
    6. }

    因此,CreatePerson会以工作单元的形式来管理数据库连接和事务,两个仓储使用同一个工作单元。

    对于工作单元特性的某些选项,可以查阅 工作单元详情章节

    IUnitOfWorkManager

    第二种方式是使用 IUnitOfWorkManager.Begin(…) 方法,如下所示:

    如上所示,你可以注入并且使用IUnitOfWorkManager(某些基类默认已经实现了 UnitOfWorkManager 的注入:MVC的控制器,Application Service,Domain Service 等等)。因此,你可以对工作单元创建更多的限制性作用域。以这种方式,你可以手动调用 Complete 方法。如果你不调用,事务会回滚并且所有更改都不会被保存。

    ABP对begin方法有很多的重载,可以用来设置 工作单元的选项。如果没有其它更好的意图,那么最好是使用 UnitOfWork 特性,因为它更方便。

    1. 禁用工作单元

    1. [UnitOfWork(IsDisabled = true)]
    2. public virtual void RemoveFriendship(RemoveFriendInput input)
    3. {
    4. _friendshipRepository.Delete(input.Id);
    5. }

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

    • 你可能想在限制性的作用域中使用UnitOfWork,我们可以使用 UnitOfWork 特性中的 Scope 参数 来设置,该参数类型是 TransactionScopeOption.aspx)。

    注意,如果工作单元方法调用这个RemoveFriendship方法,这个方法是禁用且忽略的,并且它和调用它的方法使用同一个工作单元。因此,要谨慎的使用禁用功能。同样地,上述代码能很好的工作,因为仓储方法是默认为工作单元的。

    2. 非事务性工作单元

    工作单元默认是事务性的(这是它的天性)。因此,ABP 启动/提交/回滚 一个显式的数据库级事务。在有些特殊案例中,事务可能会导致一些问题,因为它可能会锁住数据库中的某些数据行或是数据表。在此这些情况下, 你或许会想要禁用数据库级事务。我们可以使用 UnitOfWork 特性来设置它构造函数中的 isTransactional 参数来配置一个非事务的操作。示例如下:

    1. [UnitOfWork(isTransactional: false)]
    2. public GetTasksOutput GetTasks(GetTasksInput input)
    3. {
    4. var tasks = _taskRepository.GetAllWithPeople(input.AssignedPersonId, input.State);
    5. {
    6. Tasks = Mapper.Map<List<TaskDto>>(tasks)
    7. };
    8. }

    我建议这样使用这个特性 [UnitOfWork(isTransaction:false)]。这样即明确而且可读性也更好。但你同样可以使用[UnitOfWork(false)]。

    这里有一个非事务性UoW的限制。如果你已经在事务性的工作单元作用域内,即使你设定 isTransactional 为 false 这个操作也会被忽略(在一个事务单元中,使用 TransactionScopeOption 来创建一个非事务的工作单元)。

    应该谨慎的使用非事务性工作单元,因为在大多数的情况下,需要事务来维护事务的完整性。如果你的方法只是读取数据,不改变数据,那么当然可以采用非事务性的。

    3. 工作单元调用其它工作单元

    工作单元是可嵌套的,如果某个工作单元的方法,调用其它工作单元的方法,它们共享相同的连接和事务。第一个方法管理连接,而其它则使用该连接。

    4. 工作单元作用域

    你可以在其它事务中创建一个不同于该事务的隔离的事务,或者在该事务中创建一个非事务作用域。.NET 定义了 TransactionScopeOption.aspx) 类来实现这样的做法。你可以设置工作单元的作用域选项来控制它。

    5. 自动保存更改

    如果某个方法是工作单元的,ABP会在方法执行的末尾自动保存所有的更改,假设我们需要一个更新person名字的方法:

    这样,名字就被修改了!我们甚至没有调用_personRepository.Update方法。ORM框架会在工作单元内持续追踪实体所有的变化,且反映所有变化到数据库中。

    6. IRepository.GetAll() 方法

    当你在仓储外调用GetAll方法方法时,数据库的连接必须是开启的,因为它返回IQueryable类型的对象。这是必须的,因为IQueryable对象是延迟执行的,它并不会马上执行数据库查询,直到你调用ToList()方法或在foreach循环中使用IQueryable(或以某种方式访问查询项时)。因此,当你调用ToList()方法,数据库连接必需是启用状态。

    请看下面示例:

    1. [UnitOfWork]
    2. public SearchPeopleOutput SearchPeople(SearchPeopleInput input)
    3. {
    4. //取得 IQueryable<Person>
    5. var query = _personRepository.GetAll();
    6. //若有选取,则添加一些过滤条件
    7. query = query.Where(person => person.Name.StartsWith(input.SearchedName));
    8. }
    9. if(input.IsActive.HasValue) {
    10. }
    11. //取得分页结果集
    12. var people = query.Skip(input.SkipCount).Take(input.MaxResultCount).ToList();
    13. return new SearchPeopleOutput { People = Mapper.Map<List<PersonDto>>(people) };
    14. }

    在这里,SearchPeople方法必需是工作单元,因为IQueryable的ToList()方法在方法体内调用,在执行IQueryable.ToList()方法的时候,数据库连接必须开启。

    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方法来实现。

    1. public class SimpleTaskSystemCoreModule : AbpModule
    2. {
    3. public override void PreInitialize() {
    4. Configuration.UnitOfWork.IsolationLevel = IsolationLevel.ReadCommitted;
    5. Configuration.UnitOfWork.Timeout = TimeSpan.FromMinutes(30);
    6. }
    7. //...其它模块方法
    8. }

    其次,我们可以覆写某个特定工作单元的默认值。例如:使用 UnitOfWork特性的构造函数或者 IUnitOfWorkManager.Begin 方法。

    最后,你可以为ASP.NET MVC,Web API 以及 ASP.NET Core MVC 控制器使用启动配置来配置工作单元。

    工作单元是无缝工作且不可视的。但是,在有些特例下,你需要调用它的方法。

    你可以在下面两个方式中选取一个来访问当前工作单元:

    • 如果你的类是派生自某些特定的基类(ApplicationService, DomainService, AbpController, AbpApiController 等等),那么你可以直接使用 CurrentUnitOfWork 属性。

    SaveChanges

    在工作单元结束时ABP会保存所有的更改,而你不需要做任何事情。但是有些时候,你或许会想要在工作单元的执行过程中就保存更改。例如:使用EntityFramerok保存一个新实体的时候取得一个这个的ID。

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

    3.5.7 事件

    工作单元具有 Completed,Failed 以及 Disposed 事件。你可以注册这些事件并且执行所需的操作。例如:当前工作单元成功执行完成,你可能想运行某些代码: