基于DDD的.NET开发框架 - ABP工作单元(Unit of Work)

返回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.IsDeletedselect 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(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);
}

 

转载于:https://www.cnblogs.com/yinrq/p/5543046.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/294011.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

中国这门失传已久的武林绝学,竟让研究它的老外拿了诺贝尔奖

全世界只有3.14 % 的人关注了爆炸吧知识欲练神功学好物理中国武术&#xff0c;博大精深。能够掌握一门武林绝学&#xff0c;是全世界男人的终极梦想。可翻翻公认的武林十大绝学&#xff1a;葵花宝典、六脉神剑、九阳神功....不是断子绝孙&#xff0c;就是杀人于无形&#xff0c…

中国地区三级联动下拉菜单的实现

1.首先是js文件(area.js)&#xff1a; 1 function Dsy()2 {3 this.Items {};4 }5 Dsy.prototype.add function(id,iArray)6 {7 this.Items[id] iArray;8 }9 Dsy.prototype.Exists function(id)10 {11 if(typeof(this.Items[id]) "undefined") return false;12 r…

Java里面的arraycopy总结

当我们需要两个数组之和建立一个新的数组的时候&#xff0c;我以为数组也像字符串一样&#xff0c;相加就可以&#xff0c;错了&#xff0c;需要用到arraycopy arraycopy(被复制的数组, 从第几个元素开始复制, 要复制到的数组, 从第几个元素开始粘贴, 一共需要复制的元素个数);…

C#的dapper使用

Dapper是.NET下一个micro的ORM&#xff0c;它和Entity Framework或Nhibnate不同&#xff0c;属于轻量级的&#xff0c;并且是半自动的。Dapper只有一个代码文件&#xff0c;完全开源&#xff0c;你可以放在项目里的任何位置&#xff0c;来实现数据到对象的ORM操作&#xff0c;体…

android什么是回调,Android中的回调是什么?

小编典典回调的概念是在另一个类中完成某些工作时通知类同步/异步。有人称其为好莱坞原则&#xff1a;“不要叫我们&#xff0c;我们叫你”。这是一个例子&#xff1a;class A implements ICallback {MyObject o;B b new B(this, someParameter);Overridepublic void callback…

Andorid之bitmap里面的压缩总结

public Bitmap decodebitmap(Context context, int imageid) {BitmapFactory.Options options = new BitmapFactory.Options();options.inJustDecodeBounds = true;// 如果值设为true,那么将不返回实际的bitmap,也不给其分配内存空间,这样就避免了内存溢出。Bitmap bitmap =…

Android -- 创建XML文件对象及其序列化, pull解析XML文件

1. 创建XML文件对象及其序列化 示例代码&#xff1a;&#xff08;模拟以xml格式备份短信到SD卡&#xff09; SmsInfo.java&#xff0c; bean对象 /*** 短信的业务bean* author Administrator**/ public class SmsInfo {private String body;private String number;private int …

.NET大会2021参会笔记

全面拥抱Linux拥抱linux是微软的战略转型。Satya Nadella写的书《refresh》&#xff0c;就提到了MS loves Linux。所以&#xff0c;大会一开始&#xff0c;以然是Scott Hanselman&#xff0c;给我们演示如果在linux上面使用.net。好了&#xff0c;好了&#xff0c;你不用说了&a…

zookeeper配置文件详解

zoo.cfg配置文件 # The number of milliseconds of each tick tickTime2000 # The number of ticks that the initial # synchronization phase can take initLimit10 # The number of ticks that can pass between # sending a request and getting an acknowledgement sync…

html 图片 填充方式,css怎么让图片填满?

在css中&#xff0c;可以将div的高度和宽度属性设置为100%&#xff0c;同时使用background-size属性设置背景图片为100%&#xff0c;便可以实现背景图片铺满屏幕。css怎么让图片填满&#xff1f;1、新建一个HTML文件&#xff0c;使用div标签创建一个模块&#xff0c;并设置其cl…

solrcloud线上创建collection,修改默认配置

一、先看API&#xff0c;创建collection1、上传配置文件到zookeeper1&#xff09; 本地内嵌zookeeper集群&#xff1a;java -classpath ./solr-webapp/webapp/WEB-INF/lib/* org.apache.solr.cloud.ZkCLI -cmd upconfig -zkhost localhost:9983,localhost:8574,localhost:9900 …

Android之判断手机黑屏以及锁屏

1、黑屏 /** * 判断是否黑屏 * param c * return */ public final static boolean isScreenLocked(Context c) { android.app.KeyguardManager mKeyguardManager (KeyguardManager) c.getSystemService(c.KEYGUARD_SERVICE); return !mKeyguardManager.inKeyguardRestricte…

豆瓣评分9.4!这一部纪录片,探秘中国的未至之境!

全世界只有3.14 % 的人关注了爆炸吧知识Bilibili 联合“美国国家地理”&#xff0c;悄悄出品了一部史诗级动物记录片&#xff0c;忍不住要推荐给大朋友小朋友们——《未至之境》。这部纪录片由B站和国家地理联合创作&#xff0c;从绵延万里的山脉高原到枝繁叶茂的雨林竹海&…

html5设置不缓存页面,页面的缓存与不缓存设置

HTML的HTTP和谈头信息中把握着页面在几个处所的缓存信息&#xff0c;包含浏览器端&#xff0c;中心缓存办事器端(如&#xff1a;squid等)&#xff0c;Web办事器端。本文评论辩论头信息 中带缓存把握信息的HTML页面(JSP/Servlet生成好出来的也是HTML页面)在中心缓存办事器中的缓…

Ubuntu 10.10, 11.04, 11.10这三个版本无法从优盘启动

问题&#xff1a;Ubuntu 10.10, 11.04, 11.10这三个版本无法从优盘启动 解决&#xff1a;从U盘启动安装的时候&#xff0c;会卡住不动。搞定办法相当简单&#xff0c;修改syslinux/syslinuxfg文件&#xff1a;将default vesamenu32这句话注释掉即可&#xff0c;即&#xff1a;将…

.NET内存性能分析指南

.NET Memory Performance Analysis知道什么时候该担心&#xff0c;以及在需要担心的时候该怎么做译者注作者信息&#xff1a;Maoni Stephens - 微软架构师&#xff0c;负责.NET Runtime GC设计与实现 博客链接 Github译者&#xff1a;Bing Translator、INCerry 博客链接&#x…

解决php连接mysql数据库中文乱码问题

首先数据库编码和Mysql连接校对编码要一致&#xff1a; 其次在php文件中加入这两句&#xff1a; 2013-3-21更新&#xff1a; Linux下最好都用UTF-8编码&#xff1a; 1、数据库里面选utf-8_general_ci 2、php文件加上header("Content-Type: text/html; charset utf-8"…

使用反射将DataTable的数据转成实体类

利用反射避免了硬编码出现的错误&#xff0c;但是实体类的属性名必须和数据库名字对应&#xff08;相同&#xff09; 1、利用反射把DataTable的数据写到单个实体类 /// <summary>///利用反射把DataTable的数据写到单个实体类/// </summary>/// <typeparam name&…

Andorid之KeyguardManager的介绍

android.app.KeyguardManager类用于对Keyguard进行管理&#xff0c;即对锁屏进行管理。 详细信息参考&#xff1a; http://blog.csdn.net/hudashi/article/details/7073373 下面的代码用来设定键盘锁和解锁 //声明键盘管理器并获取键盘的服务 KeyguardManager keyguardManage…