解析ABP框架中的事务处理和工做单元,ABP事务处理

通用链接和事务管理方法
链接和事务管理是使用数据库的应用程序最重要的概念之一。当你开启一个数据库链接,何时开始事务,如何释放链接...诸如此类的。html

正如你们都知道的,.Net使用链接池(connection pooling)。所以,建立一个链接其实是从链接池中取得一个链接,会这么作是由于建立新链接会有成本。若是没有任何链接存在于链接池中,一个新的链接对象会被建立而且添加到链接池中。当你释放链接,它其实是将这个链接对象送回到链接池。这并非实际意义上的释放。这个机制是由.Net所提供的。所以,咱们应该在使用完以后释放掉链接对象。这就是最佳实践。数据库

在应用程序中,有两个通用的方来建立/释放一个数据库链接:安全

第一个方法:在Web请求到达的时候,建立一个链接对象。(Application_BeginRequest这个位于global.asax中的事件),使用同一个链接对象来处理全部的数据库操做,而且在请求结束的时候关闭/释放这个链接 (Application_EndRequest事件)。app

这是个简易但却没效率的方法,缘由:框架

  • 或许这个Web请求不须要操做数据库,可是链接却会开启。这对于链接池来讲是个毫无效率的使用方式。
  • 这可能会让Web请求的运行时间变长,而且数据库操做还会须要一些执行。这也是一种没效率的链接池使用方式。
  • 这对于Web应用来讲是可行的。若是你的应用程序是Widnows Service,这可能就没法被实现了。
  • 一样的这是一个使用事务式的数据库操做最佳场景。若是有一个操做发生失败,全部的操做都会回滚。由于事务会锁住数据库中的一些数据列(事件数据表),它一定要是短暂的。

第二个方法: 建立一个链接当须要的时候(只要在使用它以前)而且释放它在使用它以后。这是至关高效的,可是就得乏味并且反复的去进行(建立/释放链接)。ide

ABP的链接和事务管理
ABP综合上述两个链接管理的方法,而且提供一个简单并且高效的模型。spa

1.仓储类(Repository classes)线程

仓储是主要的数据库操做的类。ABP开启了一个数据库链接而且在进入到仓储方法时会启用一个事务。所以,你能够安全地使用链接于仓储方法中。在仓储方法结束后,事务会被提交而且会释放掉链接。假如仓储方法抛出任何异常,事务会被回滚而且释放掉链接。在这个模式中,仓储方法是单元性的(一个工做单元unit of work)。ABP在处理上述那些动做都是全自动的。在这里,有一个简单的仓储:代理

public class ContentRepository : NhRepositoryBase<Content>, IContentRepository
{
  public List<Content> GetActiveContents(string searchCondition)
  {
    var query = from content in Session.Query<Content>()
          where content.IsActive && !content.IsDeleted
          select content;

    if (string.IsNullOrEmpty(searchCondition))
    {
      query = query.Where(content => content.Text.Contains(searchCondition));
    }

    return query.ToList();
  }
}

 

 

这个示例使用NHibernate做为ORM框架。如上所示,不须要撰写任何数据库链接操做(NHibernate中的Session)的程序代码。rest

假如仓储方法调用另外一个仓储方法(通常来讲,若工做单元方法调用另外一个工做单元的方法),都使用同一个链接和事务。第一个被调用到的仓储方法负责管理链接和事务,而其他被它调用的仓储方法则只单纯使用无论理。

2.应用服务(Application service classes)

一个应用服务的方法也被考虑使用工做单元。若是咱们拥有一个应用服务方法以下:

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数量。两个仓储共享同一个链接和事务于这个例子中,由于这是一个应用服务的方法。ABP开启一个数据库链接而且开启一个事务于进入到CreationPerson这个方法,若没有任何异常抛出,接着提交这个事务于方法结尾时,如有异常被抛出,则会回滚这个事务。在这种机制下,全部数据库的操做在CreatePerson中,都成了单元性的了(工做单元)。

3.工做单元(Unit of work)

工做单元在后台替仓储和应用服务的方法工做。假如你想要控制数据库的链接和事务,你就须要直接操做工做单元。下面有两个直接使用的示例:

首要且最好的使用UnitOfWorkAttribute的方式以下:

[UnitOfWork]
public void CreatePerson(CreatePersonInput input)
{
  var person = new Person { Name = input.Name, EmailAddress = input.EmailAddress };
  _personRepository.Insert(person);
  _statisticsRepository.IncrementPeopleCount();
}

 

所以,CreatePerson方法转变成工做单元而且管理数据库链接和事务,两个仓储对象都使用相同的工做单元。要注意,假如这是应用服务的方法则不须要添加UnitOfWork属性,见工做单元方法:第三章,3.3.5。

第二个示例是使用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,如上所示。所以,你能够建立更多的有限范围 (limited scope)的工做单元。在这个机制中,你一般能够手动调用Complete方法。若是你不调用,事务会回滚而且全部的异常都不会被储存。Begin方法被重写从而设置工做单元的选项。

这很棒,不过除非你有很好的理由,不然仍是少用UnitOfWork属性。

工做单元
1.禁用工做单元(Disabling unit of work)

你或许会想要禁用应用服务方法的工做单元(由于它默认是启用的)。要想作到这个,使用UnitOfWorkAttribute的IsDisabled属性。示例以下:

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

 

 日常时, 你不会须要这么作,这是由于应用服务的方法都应该是单元性且一般是使用数据库。在有些状况下,你或许会想要禁用应用服务的工做单元:

(1)你的方法不须要任何数据库操做且你不想要开启那些不须要的数据库链接
(2)你想要使用工做单元于UnitOfWorkScope类的有限范围内,如上所述
注意,若是工做单元方法调用这个RemoveFriendship方法,禁用被忽略且它和调用它的方法使用同一个工做单元。所以,使用禁用这个功能要很当心。一样地,上述程序代码工做的很好,由于仓储方法默认即为工做单元。

2.无事务的工做单元(Non-transactional unit of work)

工做单元默认上是具事务性的(这是它的天性)。所以,ABP启动/提交/回滚一个显性的数据库等级的事务。在有些特殊案例中,事务可能会致使问题,由于它可能会锁住有些数据列或是数据表于数据库中。在此这些情境下, 你或许会想要禁用数据库等级的事务。UnitOfWork属性能够从它的建构子中取得一个布尔值来让它如非事务型工做单元般工做着。示例为:

 [UnitOfWork(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)会在单一命令中于内部进行数据储存。假设你更新了一些的实体于非事务的UoW。即使于这个情境下全部的更新都会于单一数据库命令的工做单元尾部完成。可是,若是你直接执行SQL查询,它会当即被执行。

这里有一个非事务性UoW的限制。若是你已经位于事务性UoW区域内,设定isTransactional为false这个动做会被忽略。

使用非事务性UoW要当心,由于在大多数的状况下,数据整合应该是具事务性的。若是你的方法只是读取数据,不改变数据,那么固然能够采用非事务性。

3.工做单元调用其它工做单元(A unit of work method calls another)

若工做单元方法(一个贴上UnitOfWork属性标签的方法)调用另外一个工做单元方法,他们共享同一个链接和事务。第一个方法管理链接,其它的方法只是使用它。这在全部方法都执行在同一个线程下是可行的(或是在同一个Web请求内)。实际上,当工做单元区域开始,全部的程序代码都会在同一个线程中执行并共享同一个链接事务,直到工做单元区域终止。这对于使用UnitOfWork属性和UnitOfWorkScope类来讲都是同样的。若是你建立了一个不一样的线程/任务,它使用本身所属的工做单元。

自动化的saving changes (Automatically saving changes)

当咱们使用工做单元到方法上,ABP自动的储存全部变化于方法的末端。假设咱们须要一个可更新person名称的方法:

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

 

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

注意,这不须要在应用服务声明UnitOfWork,由于它们默认就是采用工做单元。

4.仓储接口的GetAll()方法(IRepository.GetAll())

当你在仓储方法外调用GetAll方法, 这一定得有一个开启状态的数据库链接,由于它返回IQueryable类型的对象。这是须要的,由于IQueryable延迟执行。它并不会立刻执行数据库查询,直到你调用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()被执行时开启。

一如GetAll()方法,若是须要数据库链接且没有仓储的状况下,你就必需要使用工做单元。注意,应用服务方法默认就是工做单元。

5.工做单元属性的限制(UnitOfWork attribute restrictions)

在下面情境下你可使用UnitOfWork属性标签:

(1)类全部public或public virtual这些基于界面的方法(像是应用服务是基于服务界面)
(2)自我注入类的public virtual方法(像是MVC Controller和Web API Controller)
(3)全部protected virtual方法。
建议将方法标示为virtual。你没法应用在private方法上。由于,ABP使用dynamic proxy来实现,而私有方法就没法使用继承的方法来实现。当你不使用依赖注入且自行初始化类,那么UnitOfWork属性(以及任何代理)就没法正常运做。

选项
有许多能够用来控制工做单元的选项。

首先,咱们能够在startup configuration中改变全部工做单元的全部默认值。这一般是用了咱们模块中的PreInitialize方法来实现。

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

  //...other module methods
}

 

方法
工做单元系统运做是无缝且不可视的。可是,在有些特例下,你须要调用它的方法。

SaveChanges:

ABP储存全部的变化于工做单元的尾端,你不须要作任何事情。可是,有些时候,你或许会想要在工做单元的过程当中就储存全部变化。在这个案例中,你能够注入IUnitOfWorkManager而且调用IUnitOfWorkManager.Current.SaveChanges()方法。示例中以Entity Framework在储存变化时取得新增实体的Id。注意,当前工做单元是具事务性的,全部在事务中的变化会在异常发生时都被回滚,即使是已调用SaveChange。

事件
工做单元具备Completed/Failed/Disposed事件。你能够注册这些事件而且进行所需的操做。注入IUnitOfWorkManager而且使用IUnitOfWorkManager.Current 属性来取得当前已激活的工做单元而且注册它的事件。

你或许会想要执行有些程序代码于当前工做单元成功地完成。示例:

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);
}

 

 

 

 

相关文章
相关标签/搜索