返回ABP系列

ABP是“ASP.NET Boilerplate Project (ASP.NET样板项目)”的简称。

ASP.NET Boilerplate是一个用最佳实践和流行技术开发现代WEB应用程序的新起点,它旨在成为一个通用的WEB应用程序框架和项目模板。

ABP的官方网站:http://www.aspnetboilerplate.com

ABP官方文档:http://www.aspnetboilerplate.com/Pages/Documents

Github上的开源项目:https://github.com/aspnetboilerplate

一、公共连接和事务管理方法

在使用了数据库的应用中,连接和事务管理是最重要的概念之一。何时打开一个连接,何时开始一个事务,如何释放连接等等。

你可能已经知道,Net使用了连接池。因此,创建一个连接实际上是从连接池中获取一个连接,因为因为创建一个连接是有消耗的。如果在连接池中没有可用的连接,那么会创建一个新的连接,并将该连接加入连接池。当你释放连接时,实际上是将该连接发送回给连接池,并没有完全释放。这种机制是.Net提供的立即可用的功能。因此,在我们使用完一个连接后应该立即释放,在需要的时候才创建一个新的连接。总之,最佳实践记住这八个字足矣:尽晚打开,尽早释放

在一个应用中创建或者释放一个数据库连接,通常有2种方法。

第一种方法:当Web请求开始(在Global.asax的Application_BeginRequest事件中)的时候创建一个连接,在所有的数据库操作时使用相同的连接,并且在请求结束(Application_EndRequest)时关闭或者释放该连接。这种方法很简单但是不够高效。why?

  • 在一个请求中也许没有数据库操作,但是连接已经打开了。这造成了连接池的无效使用。
  • 在一次请求中,可能请求需要消耗很长的时间而数据库操作只花费很短的时间,这也会造成连接池的无效使用
  • 这只在Web应用中是可行的。如果应用是一个Windows服务,那么可能不会实现。

以事务的方式执行数据库操作已被认为是一种最佳实践。如果一个操作失败了,那么所有的操作都会回滚。因为一个事务可以锁定数据库中的一些行(甚至表),所以它必须是短暂存活的。

第二种方法:当需要时(仅在使用前)创建一个连接,使用后立即关闭。这是最有效的,但是到处创建或者释放连接是一项重复乏味的工作。

二、ABP中的连接和事务管理

ABP兼备了这两种方法并且提供了一个简单而又有效的模型。

1、仓储类

仓储式执行数据库操作主要的类。当进入一个仓储方法时,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)打开或者关闭的代码。

如果一个仓储方法调用了其他的仓储方法(一般而言,如果一个工作单元调用了其他的工作单元方法),那么它们共享相同的连接和事务。第一个进入的方法管理连接和事务,其他方法使用相同的连接和事务。

2、应用服务

一个应用服务也被认为是一个工作单元。假设我们有一个像下面的应用服务:

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方法中的所有数据库操作都成了原子的(工作单元)。

3、工作单元

工作单元对于仓储和应用服务方法隐式有效。如果你想在其他地方控制数据库连接和事务,那么可以显式使用它。

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特性,因为代码越短越好。

三、工作单元详解

1、关闭工作单元

有时候你可能想关闭应用服务方法的工作单元(因为默认是开启的),此时,可以使用UnitOfWorkAttribute的IsDisabled属性。用法如下:

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

正常情况下,不需要关闭数据单元,因为应用服务方法应该是原子的且一般都会使用数据库。但也有些例外情况让你想要关闭应用服务方法的工作单元:

  • 方法不执行任何数据库操作而且你也不想打开一个没有必要的数据库连接。
  • 如上面描述的,你想要在一个UnitOfWorkScope类的有限作用域内使用工作单元。

注意:如果一个工作单元方法调用了这个RemoveFriendship方法,那么后者的关闭工作单元的功能将会失效,并且也会使用和调用者方法相同的工作单元。因此,要小心使用工作单元的关闭功能。

2、非事务的工作单元

工作单元默认是事务的(本质如此)。因此,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将会被忽略。

使用非事务的工作单元要小心,因为大多数时候对于数据的集成是事务的。如果你的方法只是读数据,不需要改变数据,当然该方法是可以为非事务的了。

3、工作单元方法调用其它

如果一个工作单元的方法(使用了UnitOfWork特性声明的方法)调用另一个工作单元的方法,那么它们共享相同的连接和事务。第一个方法管理连接,其他方法使用连接。这个对于运行在相同线程的方法是成立的(对于web应用则是相同的请求)。实际上,当一个工作单元作用域开始时,在同一线程执行的所有代码都共享同一个连接和事务,直到工作单元作用域结束。这对于UnitOfWork特性和UnitOfWorkScope类都是成立的。

4、工作单元作用域

在其他事务中可以创建一个不同而又隔离的事务,或者可以在一个事务中创建一个非事务的作用域。.Net中定义了TransactionScopeOption,你可以为工作单元设置作用域选项。

5、自动保存

当我们为一个方法使用了工作单元时,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特性,因为它们默认已经是工作单元了。

6、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()方法一样,如果在仓储之外需要数据库连接,那么必须使用工作单元。应用服务方法默认是工作单元。

7、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();
} //...其他模块方法
}

其次,我们可以为一个特定的工作单元重写默认值。比如,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);
}

基于DDD的.NET开发框架 - ABP工作单元(Unit of Work)的更多相关文章

  1. 线上分享-- 基于DDD的.NET开发框架-ABP介绍

    前言 为了能够帮助.Net开发者开拓视野,更好的把最新的技术应用到工作中,我在3月底受邀到如鹏网.net训练营直播间为各位学弟学妹们进行ABP框架的直播分享.同时为了让更多的.NET开发者了解ABP框 ...

  2. 基于DDD的.NET开发框架 - ABP初探

    返回ABP系列 ABP是“ASP.NET Boilerplate Project (ASP.NET样板项目)”的简称. ASP.NET Boilerplate是一个用最佳实践和流行技术开发现代WEB应 ...

  3. 基于DDD的.NET开发框架 - ABP分层设计

    返回ABP系列 ABP是“ASP.NET Boilerplate Project (ASP.NET样板项目)”的简称. ASP.NET Boilerplate是一个用最佳实践和流行技术开发现代WEB应 ...

  4. 基于DDD的.NET开发框架 - ABP仓储实现

    返回ABP系列 ABP是“ASP.NET Boilerplate Project (ASP.NET样板项目)”的简称. ASP.NET Boilerplate是一个用最佳实践和流行技术开发现代WEB应 ...

  5. 基于DDD的.NET开发框架 - ABP领域服务

    返回ABP系列 ABP是“ASP.NET Boilerplate Project (ASP.NET样板项目)”的简称. ASP.NET Boilerplate是一个用最佳实践和流行技术开发现代WEB应 ...

  6. 基于DDD的.NET开发框架ABP实例,多租户 (Sass)应用程序,采用.NET MVC, Angularjs, EntityFramework-介绍

    介绍 基于ABPZERO的多租户 (Sass)应用程序,采用ASP.NET MVC, Angularjs-介绍 ASP.NET Boilerplate作为应用程序框架. ASP.NET MVC和ASP ...

  7. 基于DDD的.NET开发框架ABP实例,多租户 (Saas)应用程序,采用.NET MVC, Angularjs, EntityFramework-介绍

    介绍 基于ABPZERO的多租户 (Saas)应用程序,采用ASP.NET MVC, Angularjs-介绍 ASP.NET Boilerplate作为应用程序框架. ASP.NET MVC和ASP ...

  8. 基于DDD的.NET开发框架 - ABP的Entity设计思想

    返回ABP系列 ABP是“ASP.NET Boilerplate Project (ASP.NET样板项目)”的简称. ASP.NET Boilerplate是一个用最佳实践和流行技术开发现代WEB应 ...

  9. 基于DDD的.NET开发框架 - ABP日志Logger集成

    返回ABP系列 ABP是“ASP.NET Boilerplate Project (ASP.NET样板项目)”的简称. ASP.NET Boilerplate是一个用最佳实践和流行技术开发现代WEB应 ...

随机推荐

  1. mysql-mmm 安装配置(双主)

    原文地址:mysql-mmm 安装配置 作者:chinaunix1116 MMM即Master-Master Replication Managerfor MySQL(mysql主主复制管理器)关于m ...

  2. 纯css实现照片墙3D效果

    每张照片都有美丽的故事.美好的回忆.家居中的照片墙则帮你展现出这些承载着家庭重要记忆的照片,除了用画框装饰照片挂在墙上外,照片墙还可以演变为手绘照片墙.也经常在网上看到一些关于照片墙的特效案例,决定自 ...

  3. Storm系列(三):创建Maven项目打包提交wordcount到Storm集群

    在上一篇博客中,我们通过Storm.Net.Adapter创建了一个使用Csharp编写的Storm Topology - wordcount.本文将介绍如何编写Java端的程序以及如何发布到测试的S ...

  4. WIN7管理工具配置ODBC数据源-系统DSN中无Oracle,Sybase驱动的解决方法

    在C:\Windows\SysWOW64下找到: odbcad32.exe 这个文件,双击打开. 点击添加按钮,选择 对应的 驱动,然后就可用添加连接Oracle/Sybase的ODBC的数据源了.

  5. x01.Lab.OpenCV: 计算机视觉

    横看成岭侧成峰,计算视觉大不同.观看的角度不同,成像自然不同,这对计算机视觉来说,是个大麻烦.但计算机视觉应用如此广泛,却又有不得不研究的理由.指纹机大家都用过吧,这不过是冰山之一角.产品检测,机器人 ...

  6. background-position控制背景位置

    提示:需要把 background-attachment 属性设置为 "fixed",才能保证该属性在 Firefox 和 Opera 中正常工作.

  7. Linux rpmbuild命令

    一.简介 rpmbuild命令用于创建软件的二进制包和源代码包. 二.选项 参考:http://blog.sina.com.cn/s/blog_4ba5b45e0102e5r2.html http:/ ...

  8. php中的curl使用入门教程和常见用法实例

    摘要: [目录] php中的curl使用入门教程和常见用法实例 一.curl的优势 二.curl的简单使用步骤 三.错误处理 四.获取curl请求的具体信息 五.使用curl发送post请求 六.文件 ...

  9. 【4412嵌入式开发板学习笔记】认识uboot

    转自迅为讨论群:http://www.topeetboard.com 重要说明:这份笔记不是4412开发配套的,是我在网上看视频的时候下载上课老师的笔记后修改的.所以我试了一下笔记上的uboot命令, ...

  10. ARM学习篇一 点亮LED

    要点亮LED,先决条件是什么,当然得有相应的硬件设施.板子的整个电路图比较大,我就直接取相关部分. 给发光二级管加上3.3v电压后,通过1k电阻,直接与S3C2440连接.至于为什么要加电阻,大家应该 ...