ABP理论学习之工作单元(Unit of Work)
本篇目录
公共连接和事务管理方法
在使用了数据库的应用中,连接和事务管理是最重要的概念之一。何时打开一个连接,何时开始一个事务,如何释放连接等等。
你可能已经知道,Net使用了连接池。因此,创建一个连接实际上是从连接池中获取一个连接,因为因为创建一个连接是有消耗的。如果在连接池中没有可用的连接,那么会创建一个新的连接,并将该连接加入连接池。当你释放连接时,实际上是将该连接发送回给连接池,并没有完全释放。这种机制是.Net提供的立即可用的功能。因此,在我们使用完一个连接后应该立即释放,在需要的时候才创建一个新的连接。总之,最佳实践记住这八个字足矣:尽晚打开,尽早释放。
这里我推荐一篇关于数据库连接的文章,写得很浅显易懂:《细说数据库连接》
在一个应用中创建或者释放一个数据库连接,通常有2种方法。
第一种方法:当Web请求开始(在Global.asax的Application_BeginRequest事件中)的时候创建一个连接,在所有的数据库操作时使用相同的连接,并且在请求结束(Application_EndRequest)时关闭或者释放该连接。这种方法很简单但是不够高效。为啥呢?
- 在一个请求中也许没有数据库操作,但是连接已经打开了。这造成了连接池的无效使用。
- 在一次请求中,可能请求需要消耗很长的时间而数据库操作只花费很短的时间,这也会造成连接池的无效使用。
- 这只在Web应用中是可行的。如果应用是一个Windows服务,那么可能不会实现。
以事务的方式执行数据库操作已被认为是一种最佳实践。如果一个操作失败了,那么所有的操作都会回滚。因为一个事务可以锁定数据库中的一些行(甚至表),所以它必须是短暂存活的。
第二种方法:当需要时(仅在使用前)创建一个连接,使用后立即关闭。这是最有效的,但是到处创建或者释放连接是一项重复乏味的工作。
ABP中的连接和事务管理
ABP兼备了这两种方法并且提供了一个简单而又有效的模型。
仓储类
仓储式执行数据库操作主要的类。当进入一个仓储方法时,ABP会打开一个数据库连接(可能不是立即打开,但是在第一次使用数据库时肯定是打开的,取决于ORM提供者的实现)并开始一个事务。因此,在一个仓储方法中可以安全地使用连接。在方法的结束,事务被提交并且连接被释放。如果仓储方法抛出任何异常,那么事务都会回滚且连接被释放。这样一来,仓储方法就是原子的(一个工作单元)。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)打开或者关闭的代码。
如果一个仓储方法调用了其他的仓储方法(一般而言,如果一个工作单元调用了其他的工作单元方法),那么它们共享相同的连接和事务。第一个进入的方法管理连接和事务,其他方法使用相同的连接和事务。
应用服务
一个应用服务也被认为是一个工作单元。假设我们有一个像下面的应用服务:
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仓储增加总人数。在这里例子中,这两个仓储共享相同的连接和事务,因为它们在一个应用服务方法中。ABP在进入CreatePerson方法时打开一个数据库连接并开始一个事务,如果没有抛出异常事务会在方法结尾时提交,如果有任何异常发生,将会回滚。这样一来,在CreatePerson方法中的所有数据库操作都成了原子的(工作单元)。
工作单元
工作单元对于仓储和应用服务方法隐式有效。如果你想在其他地方控制数据库连接和事务,那么可以显式使用它。
UnitOfWork特性
最受人欢迎的方法是使用UnitOfWorkAttribute。例如:
[UnitOfWork]
public void CreatePerson(CreatePersonInput input)
{
var person = new Person { Name = input.Name, EmailAddress = input.EmailAddress };
_personRepository.Insert(person);
_statisticsRepository.IncrementPeopleCount();
}
这样,CreatePerson方法变成了工作单元并且管理数据库连接和事务,两个仓储使用相同的工作单元,注意的是,如果这是一个应用服务方法,就不需要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();
}
}
}
你可以注入然后使用IUnitOfWork,正如这里演示的这样(如果你的应用继承自ApplicationService类,那么你可以直接使用CurrentUnitOfWork属性。如果没有,你要先注入IUnitOfWorkManager)。这样,你就可以创建更多的限制作用域的工作单元。用这种方法,你应该手动调用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框架(如EF和NH)内部使用了一条单一命令来保存更改。假设你以非事务的UOW(工作单元)更新了一些实体的情景,甚至在这种情况下所有的更新都是在工作单元结束时以一个单一的数据库命令执行的。但是如果你直接执行一个SQL查询,它会立即执行。
非事务的UOW有一个限制。如果你已经处于一个事务的工作单元的作用域内,那么将isTransactional设置为false将会被忽略。
使用非事务的工作单元要小心,因为大多数时候对于数据的集成是事务的。如果你的方法只是读数据,不需要改变数据,当然该方法是可以为非事务的了。
工作单元方法调用其它
如果一个工作单元的方法(使用了UnitOfWork特性声明的方法)调用另一个工作单元的方法,那么它们共享相同的连接和事务。第一个方法管理连接,其他方法使用连接。这个对于运行在相同线程的方法是成立的(对于web应用则是相同的请求)。实际上,当一个工作单元作用域开始时,在同一线程执行的所有代码都共享同一个连接和事务,直到工作单元作用域结束。这对于UnitOfWork特性和UnitOfWorkScope类都是成立的。
工作单元作用域
在其他事务中可以创建一个不同而又隔离的事务,或者可以在一个事务中创建一个非事务的作用域。.Net中定义了TransactionScopeOption,你可以为工作单元设置作用域选项。
自动保存
当我们为一个方法使用了工作单元时,ABP会在该方法结束时自动保存所有的更改。假设我们有一个更新person的name的方法:
[UnitOfWork]
public void UpdateName(UpdateNameInput input)
{
var person = _personRepository.Get(input.PersonId);
person.Name = input.NewName;
}
你要做的就这么多,person的name就改变了。我们甚至不用调用_personRepository.Update方法。ORM框架会跟踪工作单元中实体的所有改变,并将改变反应给数据库。
注意没有必要为应用服务方法声明UnitOfWork特性,因为它们默认已经是工作单元了。
IRepository.GetAll()方法
当在一个仓储方法之外调用GetAll()时,必须存在一个打开的数据库连接,因为GetAll返回了IQueryable,而且IQueryable会延迟执行。直到调用ToList()方法或者在foreach循环中使用IQueryable,才会真正执行数据库查询。因此,调用ToList()方法时,数据库连接必须是活着的(alive)。
考虑一下下面的例子:
[UnitOfWork]
public SearchPeopleOutput SearchPeople(SearchPeopleInput input)
{
//返回IQueryable<Person>
var query = _personRepository.GetAll();
//添加一些过滤
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);
}
//获得分页结果列表
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()方法一样,如果在仓储之外需要数据库连接,那么必须使用工作单元。注意,应用服务方法默认是工作单元。
UnitOfWork特性的限制
UnitOfWork可以用于以下几个条件:
- 所有用于接口的类的public或public virtual方法(如用于用于服务接口的应用服务类的方法)。
- 自注入类的所有public virtual(如MVC 控制器和Web Api控制器)。
- 所有的protected virtual方法。
建议总是将方法声明为virtual,但是不能用于private方法。因为ABP为virtual方法私有了动态代理,private方法不能被派生的类访问到。如果你没有使用依赖注入且实例化类,那么UnitOfWork特性(和任何代理)就不能工作。
选项
有很多可以用于改变工作单元行为的选项。
首先,我们可以在启动配置中更改所有工作单元的默认值。这通常是在模块的PreInitialize方法中处理的。
public class SimpleTaskSystemCoreModule : AbpModule
{
public override void PreInitialize()
{
Configuration.UnitOfWork.IsolationLevel = IsolationLevel.ReadCommitted;
Configuration.UnitOfWork.Timeout = TimeSpan.FromMinutes(30);
}
//...其他模块方法
}
其次,我们可以为一个特定的工作单元重写默认值。比如,UnitOfWork特性的构造函数和IUnitOfWorkManager的Begin方法都有获得选项的重载。
方法
UnitOfWork系统无缝而不可见地工作。但是在某些场合,你需要调用它的方法。
SaveChanges
ABP会在工作单元结束时保存所有更改,我们根本不用做任何事情。但是有时候你可能想在工作单元操作的中间将更改保存到数据库中。在这种情况下,你可以注入IUnitOfWorkManager,然后调用IUnitOfWorkManager.Current.SaveChanges()方法。注意:如果当前的工作单元是事务的,那么如果有异常发生了,事务中的所有改变都会回滚,即使是已保存的改变。
事件
工作单元有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: 给派发的人发送邮件*/ };
}
_taskRepository.Insert(task);
}
ABP理论学习之工作单元(Unit of Work)的更多相关文章
- ABP(现代ASP.NET样板开发框架)系列之12、ABP领域层——工作单元(Unit Of work)
点这里进入ABP系列文章总目录 基于DDD的现代ASP.NET开发框架--ABP系列之12.ABP领域层——工作单元(Unit Of work) ABP是“ASP.NET Boilerplate Pr ...
- ABP领域层——工作单元(Unit Of work)
ABP领域层——工作单元(Unit Of work) 点这里进入ABP系列文章总目录 基于DDD的现代ASP.NET开发框架--ABP系列之12.ABP领域层——工作单元(Unit Of work) ...
- 基于DDD的.NET开发框架 - ABP工作单元(Unit of Work)
返回ABP系列 ABP是“ASP.NET Boilerplate Project (ASP.NET样板项目)”的简称. ASP.NET Boilerplate是一个用最佳实践和流行技术开发现代WEB应 ...
- 手工搭建基于ABP的框架 - 工作单元以及事务管理
一个业务功能往往不只由一次数据库请求(或者服务调用)实现.为了功能的完整性,我们希望如果该功能执行一半时出错,则撤销前面已执行的改动.在数据库层面上,事务管理实现了这种完整性需求.在ABP中,一个完整 ...
- [ABP]浅谈工作单元 在整个 ABP 框架当中的应用
ABP在其内部实现了工作单元模式,统一地进行事务与连接管理. 其核心就是通过 Castle 的 Dynamic Proxy 进行动态代理,在组件注册的时候进行拦截器注入,拦截到实现了 Unit Of ...
- 工作单元(Unit of Work)
维护受业务事务影响的对象列表,并协调变化的写入和并发问题的解决. 从DB中存取Data时,必须记录增删改动作,以将对DB有影响的数据写会到DB中去. 如果在每次修改对象模型时就对DB进行相应的修改,会 ...
- 【.Net设计模式系列】工作单元(Unit Of Work)模式 ( 二 )
回顾 在上一篇博客[.Net设计模式系列]仓储(Repository)模式 ( 一 ) 中,通过各位兄台的评论中,可以看出在设计上还有很多的问题,在这里特别感谢 @横竖都溢 @ 浮云飞梦 2位兄台对博 ...
- 工作单元 — Unit Of Work
在进行数据库添加.修改.删除时,为了保证事务的一致性,即操作要么全部成功,要么全部失败.例如银行A.B两个账户的转账业务.一方失败都会导致事务的不完整性,从而事务回滚.而工作单元模式可以跟踪事务,在操 ...
- 解析ABP框架中的事务处理和工作单元,ABP事务处理
通用连接和事务管理方法连接和事务管理是使用数据库的应用程序最重要的概念之一.当你开启一个数据库连接,什么时候开始事务,如何释放连接...诸如此类的. 正如大家都知道的,.Net使用连接池(connec ...
随机推荐
- Hadoop单机模式安装-(3)安装和配置Hadoop
网络上关于如何单机模式安装Hadoop的文章很多,按照其步骤走下来多数都失败,按照其操作弯路走过了不少但终究还是把问题都解决了,所以顺便自己详细记录下完整的安装过程. 此篇主要介绍在Ubuntu安装完 ...
- VS Code 配置Python
1. 安装VS Code的Python插件,选择安装次数最多的那个 2. 点击用户设置菜单,在settings.json中添加 "python.pythonPath": " ...
- gdb的可视化工具安装
红帽推出的insight https://www.sourceware.org/insight/index.php http://wiki.ubuntu.org.cn/Insight%E7%9A%84 ...
- 查看Linux系统版本与位数
查看系统发行版信息 查看LSB (Linux Standard Base)本身的版本信息. .el5 .el5 .el5 -bit LSB executable, Intel 80386, ver ...
- Oracle的tnsnames.ora配置(PLSQL Developer)
首先打开tnsnames.ora的存放目录,一般为D:\app\Administrator\product\11.2.0\client_1\network\admin,就看安装具体位置了. 步骤阅读 ...
- Node.js之创建应用
1.使用Node.js时,不仅仅在实现一个应用,同时实现了整个HTTP服务器: 2.Node.js由下列几部分组成: (1)引入required模块:我们可以使用require指令来载入Node.js ...
- python学习 3笔记
merge dict def merge(defaults, override): r = {} for k, v in defaults.items(): if k in override: if ...
- XE2 IntraWeb尝试
新建--选择--确定 简单放几个控件在TIWForm1上面,运行,生成一个项目服务器: 复制 URL http://127.0.0.1:8888/$/start 打开网页.
- Java设置session超时(失效)的三种方式
1. 在web容器中设置(此处以tomcat为例) 在tomcat-6.0\conf\web.xml中设置,以下是tomcat 6.0中的默认配置: <!-- ================= ...
- SpringMvc的简单介绍
1.mcv框架要做哪些事情 (a)将url映射到java类或者Java类的方法 (b)封装用户提交的数据 (c)处理请求---调用相关的业务处理,封装响应的数据 (d)将封装的数据进行渲染,jsp,h ...