本节内容:
简介
在一个使用数据库的应用里,连接和事务是非常重要的,何时打开一个连接,何时开启一个事务,如果释放连接等等。ABP通过工作单元系统管理数据库的连接和事务。
在ABP中管理连接和事务
当进行一个工作单元方法,ABP打开一个数据库连接(可能不是马上打开,在第一次用到数据库时打开,这要看ORM供应器的实现了)和开始一个事务,所以你可以安全地在这个方法内使用连接,在这个方法的最后,提交事务并释放连接,如果这个方法抛出任何的异常,回滚事务并释放连接,这种方式,一个工作单元就是原子性的。ABP自动完成这些操作。
如果一个工作单元方法调用另一个工作单元方法,两个会使用同一个连接和事务,第一个方法管理连接和事务,另一个使用它们。
约定的工作单元方法
有些方法默认为工作单元方法:
- 所有Mvc、Web Api和Asp.net Core Mvc控制器的Actions。
- 所有应用服务方法。
- 所有仓储方法。
假设我们有一个应用服务方法,如下所示:
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();
} //...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