工作单元
- 介绍
- ABP中的连接和事务管理
- 工作单元详情
- 选项
- 方法
- 事件
介绍
在使用数据库的应用中,连接和事务管理是最重要的概念之一。什么时候打开连接,什么时候开始一个事务,如何释放连接等等。ABP使用工作单元系统来管理连接和事务。
ABP中的连接和事务管理
当进入到一个工作单元方法时,ABP打开一个数据库连接(可能不会立即打开,但在首次使用时打开的,基于ORM提供者如何实现)并开始一个事务。所以,你可以在这个方法中安全的使用连接,在方法的结尾处,事务会提交,连接被释放。如果方法抛出了任何异常,事务会回滚,连接被释放。使用这种方式,工作单元方法是原子的(一个工作单元)。ABP自动完成这些操作。
如果一个工作单元方法调用另一个工作单元方法,两者使用相同的连接和事务。首次进入的方法管理连接和事务,其他的方法使用它。
传统的工作单元方法
一些方法默认是工作单元方法:
- 所有的MVC、Web API和ASP.NET Core MVC控制器操作。
- 所有的应用服务方法。
- 所有的仓储方法。
假定我么有一个应用服务方法,如下:
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,并使用静态仓储增加people的总数。在这个例子中,两个仓储共享相同的连接和事务,因为应用服务方法默认是一个工作单元。当进入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特性有些选项。参见“工作单元详情”部分
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特性,它更好且简短。
工作单元详情
禁用工作单元
你可能希望禁用传统工作单元方法的工作单元,可以使用UnitOfWorkAttribute特性的IsDisabled属性。示例用法:
[UnitOfWork(IsDisabled = true)] public virtual void RemoveFriendship(RemoveFriendshipInput input) { _friendshipRepository.Delete(input.Id); }
正常来讲,你不希望这样做,但是在某些场景下,你或许希望禁用工作单元。
- 你或许希望使用UnitOfWorkScope类将工作单元在一个限制范围内使用,如上描述的那样。
注意,如果一个工作单元方法调用这个RemoveFriendShip方法,禁用这个方法会被忽略,它和调用方法使用相同的工作单元。所以,使用禁止需要小心。因为仓储方法默认为工作单元的,所以上面这些代码可以良好的工作。
无事务工作单元
工作单元默认是事务的(本性如此)。因此,ABP开始/提交/回滚一个显示的数据库级别的事务。在某些特殊情况下,事务会导致问题,因为它可能会锁定数据库中的行或表。在这种情况下,你或许会希望禁用数据库级别的事务。UnitOfWork特性可以在它的构造函数中获取一个布尔值来以无事务的方式工作。
示例用法:
[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)内部在一个命令里保存更改。假定,你在一个无事务UOW里更新了一些实体。即使在这种情况下,所有的更新操作都在工作单元的结尾使用一个数据库命令来执行。但是,如果你直接执行一个SQL查询,他会立即执行并且不会回滚,如果UOW是无事务的话。
无事务UOWs有一个限制。如果你在一个事务工作单元范围内,设置isTransactional为false会被忽略(使用事务范围选项在一个事务工作单元中创建一个无事务工作单元)。
需要小心使用无事务工作单元,因为大多数时候为了确保数据完成性应使用事务。如果你的方法仅仅读取数据而不改变他,那么它可以是无事务且安全的。
一个工作单元方法调用另一个
工作单元是环绕的。如果一个工作单元的方法调用另一个工作单元的方法,他们共享同样的连接和事务。第一个方法管理连接,其他的使用它。
工作单元范围
你可以在另一个事务里创建一个不同和隔离的事务或者在一个事务里创建一个非事务范围。.NET定义了TransactionScopeOption来实现这个功能,你可以设计工作单元的范围选项来控制它。
自动保存更改
如果一个方法时工作单元的,ABP自动在方法结束时保存所有的更改。假定,我们需要更新person名字的方法:
[UnitOfWork] public void UpdateName(UpdateNameInput input) { var person = _personRepository.Get(input.PersonId); person.Name = input.NewName; }
就这样,名字被更改了!我们甚至不用调用_personRepository.Update方法。ORM框架在一个工作单元里保持跟踪实体的所有跟踪,并且将更改反映到数据库中。
注意,对于传统的工作单元方法不需要声明UnitOfWork。
IRepository.GetAll()方法
当你在仓储方法之外调用GetAll()时,必须有一个打开的数据库连接,因为它返回的时IQueryable。z这是必须的,因为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()方法执行时,数据库连接必须是打开的。
在大多数情况下,在web应用里使用GetAll方法是安全的,因为所有的控制器动作默认都是工作单元的,因此在整个请求中数据库连接都是可用的。
工作单元特性限制
你可以这样使用UnitOfWork特性:
- 基于接口使用的类(如应用服务基于服务接口使用)的所有的public或public virtual方法。
- 自注入类(如MVC Controllers和WebAPI Controllers)的所有public virtual方法。
- 所有的protected virtual方法。
建议使用虚方法,但是不能做为私有方法使用。因为,ABP为虚方法使用动态代理,私有方法对继承类是不可见的。如果你不使用动态注入并实例化类, UnitOfWork特性(和任何代理)将不能工作。
选项
这有一些选项可以用来更改工作单元的行为。
首先,我们可以在启动配置里,更改所有工作单元的默认值。通常在我们模块的PreInitialize方法中实现。
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控制器(参见他们的文档)默认的工作单元特性。
方法
工作单元系统无缝的工作且不可见的。但是,在一些特殊情况下,你需要调用它的方法。
你可以使用两种方式中的一种访问当前的工作单元:
- 你可以直接使用CurrentUnitOfWork属性,如果你的类是继承自一些特殊的基类(应用服务、领域服务、AbpController、AbpApiController...等等)。
- 你可以在任何类中注入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); }