一个业务功能往往不只由一次数据库请求(或者服务调用)实现。为了功能的完整性,我们希望如果该功能执行一半时出错,则撤销前面已执行的改动。在数据库层面上,事务管理实现了这种完整性需求。在ABP中,一个完整的业务功能称为一个工作单元(Unit of Work,简称UoW)。工作单元代表一种完整的、原子性的操作。即一个工作单元包含的步骤要么全部被执行,要么都不被执行。如果执行一半时出现异常,则必须讲已执行的步骤还原。通常我们将事务管理实现在工作单元中。下面我们从ABP源码入手研究如何使用工作单元。

ABP工作单元(UoW)的工作原理

ABP默认将工作单元应用在Repositories、 Application Services、MVC控制器和Web API控制器等组件。也就是说,这些组件的每个方法都是一个工作单元。ABP文档对工作单元的原理讲得不是很详细,所以我们只能通过源码进行研究。这里我们以MVC控制器为例来了解一下ABP工作单元大致的工作原理。源码分析比较枯燥,最好配套ABP源码阅读,或者跳到后面看粗体字结论

ABP在Web模块初始化时注册了过滤器AbpMvcUowFilterAbpMvcUowFilter在请求处理前(OnActionExecuting方法)调用UnitOfWorkManager.Begin方法来开始一个工作单元。UnitOfWorkManager.Begin创建一个IUnitOfWork的实例并赋值给ICurrentUnitOfWorkProvider.Current,然后调用IUnitOfWork.Begin方法开始一个工作单元。在请求处理结束后(OnActionExecuted方法)如果处理过程没有异常就调用IUnitOfWork.Complete方法完成工作单元,并且无论请求处理是否成功,都调用IUnitOfWork.Dispose来结束工作单元。

ABP提供了一个实现IUnitOfWork的抽象基类UnitOfWorkBase,另外还有个继承了UnitOfWorkBase的类NullUnitOfWorkNullUnitOfWork定义上面有一段注释如此写到:

/// <summary>
/// Null implementation of unit of work.
/// It's used if no component registered for <see cref="IUnitOfWork"/>.
/// This ensures working ABP without a database.
/// </summary>
public sealed class NullUnitOfWork : UnitOfWorkBase

NullUnitOfWork是一个“空”的工作单元,它不会做任何操作。如果我们没有在IoC容器中注册其它IUnitOfWork的实现类,则ABP默认使用不做任何事的NullUnitOfWork作为工作单元。所以如果我们要做一些保证功能完整性的工作(比如开启数据库事务),就要实现IUnitOfWork并注册到IoC容器

阅读UnitOfWorkBase可以看到,UnitOfWorkBase分别在Begin方法、Complete方法和Dispose方法中调用了BeginUow方法、CompleteUow方法和DisposeUow方法。我们需要重写的主要是BeginUowCompleteUowDisposeUow这三个方法

通过源码简单了解了原理后,我们后面写代码要注意的有下面几点:

  1. 写一个继承UnitOfWorkBase的类UnitOfWork,并实现接口ITransientDependency保证UnitOfWork被注册到IoC容器
  2. 重写方法UnitOfWorkBase.BeginUow,实现工作单元开始时的启动操作
  3. 重写方法UnitOfWorkBase.CompleteUow,实现工作单元正常结束时的保存操作
  4. 重写方法UnitOfWorkBase.DisposeUow,实现工作单元结束时的清理操作
  5. 通过ICurrentUnitOfWorkProvider.Current来获取当前的工作单元

重写SessionProvider,并实现工作单元

在之前文章(手工搭建基于ABP的框架(2) - 访问数据库)实现的LocalDbSessionProvider中,为了追求代码简单,我们粗暴地用一个实质上是全局的变量来保存数据库Session,在每次访问数据库时,flush上一个Session并创建新Session。另一方面,数据库连接的配置、Session的创建保存、以及Session的提供都胡乱地放在了这个类里。这其实是非常不合理而且会引发很多问题的实现方法。

下面我们重新设计这一块逻辑。我们将LocalDbSessionProvider所负责的功能拆分,分别实现在LocalDbSessionConfigurationUnitOfWorkUnitOfWorkLocalDbSessionProvider三个类中:

  • LocalDbSessionConfiguration,单例。实现数据库连接配置,提供数据库Session工厂。

    public class LocalDbSessionConfiguration : ILocalDbSessionConfiguration, IDisposable
    {
    protected FluentConfiguration FluentConfiguration { get; private set; } public ISessionFactory SessionFactory { get; } public LocalDbSessionConfiguration()
    {
    FluentConfiguration = Fluently.Configure();
    // 数据库连接串
    var connString = "data source=|DataDirectory|MySQLite.db;";
    FluentConfiguration
    // 配置连接串
    .Database(SQLiteConfiguration.Standard.ConnectionString(connString))
    // 配置ORM
    .Mappings(m => m.FluentMappings.AddFromAssembly(Assembly.GetExecutingAssembly()));
    // 生成session factory
    SessionFactory = FluentConfiguration.BuildSessionFactory();
    } public void Dispose()
    {
    SessionFactory.Dispose();
    }
    }
  • UnitOfWork,管理数据库Session的生命期。数据库Session和事务的创建、销毁都封装在这里。这里的一个问题是何时创建数据库Session。一个自然的想法是在BeginUow创建。然而这并不是一个很好的方式,会产生如下问题:1、默认情况下,Controllers、Application Services和Repositories都会开启工作单元,也就是说,一次HTTP请求可能会多次开启工作单元,导致过多地创建数据库Session,甚至导致数据库被锁;2、即使某个接口不需要访问数据库,工作单元仍然会创建数据库Session,浪费资源。正确的做法是在需要获取数据库Session的时候才进行创建。在下面的实现中,我们将在UnitOfWork实现一个GetOrCreateSession方法来获取数据库Session。该方法在第一次调用时创建一个数据库Session并开启事务,后续调用则返回前面已创建的数据库Session。后面UnitOfWorkLocalDbSessionProvider将调用这个方法来获取数据库Session。

    public class UnitOfWork : UnitOfWorkBase, ITransientDependency
    {
    public ILocalDbSessionConfiguration DbSessionConfiguration { get; } private ISession _session; public UnitOfWork(
    IConnectionStringResolver connectionStringResolver,
    IUnitOfWorkDefaultOptions defaultOptions,
    IUnitOfWorkFilterExecuter filterExecuter,
    ILocalDbSessionConfiguration localDbSessionConfiguration)
    : base(connectionStringResolver, defaultOptions, filterExecuter)
    {
    DbSessionConfiguration = localDbSessionConfiguration;
    } public ISession GetOrCreateSession()
    {
    if (_session == null)
    {
    _session = DbSessionConfiguration.SessionFactory.OpenSession();
    _session.BeginTransaction();
    }
    return _session;
    } public override void SaveChanges()
    {
    _session?.Flush();
    } public override Task SaveChangesAsync()
    {
    // 我们不用异步Action,就不实现这个方法了。
    throw new NotImplementedException();
    } protected override void CompleteUow()
    {
    SaveChanges();
    _session?.Transaction?.Commit();
    } protected override Task CompleteUowAsync()
    {
    // 我们不用异步Action,就不实现这个方法了。
    throw new NotImplementedException();
    } protected override void DisposeUow()
    {
    _session?.Transaction?.Dispose();
    _session?.Dispose();
    }
    } internal static class UnitOfWorkExtensions
    {
    public static ISession GetSession(this IActiveUnitOfWork unitOfWork)
    {
    if (unitOfWork == null)
    {
    throw new ArgumentNullException(nameof(unitOfWork));
    } if (!(unitOfWork is UnitOfWork))
    {
    throw new ArgumentException("unitOfWork is not type of " + typeof(UnitOfWork).FullName, nameof(unitOfWork));
    } return (unitOfWork as UnitOfWork).GetOrCreateSession();
    }
    }
  • UnitOfWorkLocalDbSessionProvider,单例。通过当前的工作单元来提供数据库Session。

    public class UnitOfWorkLocalDbSessionProvider : ISessionProvider, ISingletonDependency
    {
    private readonly ICurrentUnitOfWorkProvider _unitOfWorkProvider; public UnitOfWorkLocalDbSessionProvider(ICurrentUnitOfWorkProvider currentUnitOfWorkProvider)
    {
    _unitOfWorkProvider = currentUnitOfWorkProvider;
    } public ISession Session => _unitOfWorkProvider.Current?.GetSession();
    }

最后,TweetRepositoryTweetQueryService的构造函数用到了旧的LocalDbSessionProvider,这两处也需要改一下:

public TweetRepository()
: base(IocManager.Instance.Resolve<UnitOfWorkLocalDbSessionProvider>())
{ }
public TweetQueryService()
: base(IocManager.Instance.Resolve<UnitOfWorkLocalDbSessionProvider>())
{ }

使用NHProfiler进行验证

上面实现了工作单元并封装了数据库事务管理。我们需要有方法验证数据库访问时确实开启了事务。NHProfiler是一个能够监视NHibernate生成的SQL语句的工具。我们将使用NHProfiler查看生成的SQL,确认实现了工作单元后确实开启了事务管理。

NHProfiler由两个部分组成:

  1. 一个嵌入到我们应用的DLL。这个DLL会在NHibernate访问数据库时往本地socket发送生成的SQL语句。
  2. 客户端。这个客户端通过socket接收上面所说的DLL发送的数据并展示。

接下来我们的程序需要做一些小改动。首先MyTweet.Web项目需要引用NHProfiler包里的HibernatingRhinos.Profiler.Appender.dll。或者从Nuget添加NHibernateProfiler.Appender包。如果你从NuGet添加的,则需要确认NuGet包的版本和客户端的版本一致。

添加引用后,我们还需要在入口函数Application_Start加上HibernatingRhinos.Profiler.Appender.NHibernate.NHibernateProfiler.Initialize()这句语句来开启NHProfiler的监听:

protected override void Application_Start(object sender, EventArgs e)
{
AreaRegistration.RegisterAllAreas();
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles); HibernatingRhinos.Profiler.Appender.NHibernate.NHibernateProfiler.Initialize(); IocManager.Instance.IocContainer.AddFacility<LoggingFacility>(
f => f.UseAbpLog4Net().WithConfig("log4net.config")); base.Application_Start(sender, e);
}

现在我们启动MyTweet.Web应用,然后打开NHProfiler客户端。这时NHProfiler已经在监听NHibernate生成的SQL了。到我们的应用新建一条tweet,再回到NHProfiler客户端,可以看到,新建tweet的操作确实被事务包裹起来了。

总结

在本文中,我们自己继承UnitOfWorkBase实现工作单元,使得整个框架更灵活,更容易扩展。在现有代码上稍作修改,我们还可以支持多数据库事务。

手工搭建基于ABP的框架 - 工作单元以及事务管理的更多相关文章

  1. 手工搭建基于ABP的框架(2) - 访问数据库

    为了防止不提供原网址的转载,特在这里加上原文链接: http://www.cnblogs.com/skabyy/p/7517397.html 本篇我们实现数据库的访问.我们将实现两种数据库访问方法来访 ...

  2. 手工搭建基于ABP的框架(3) - 登录,权限控制与日志

    为了防止不提供原网址的转载,特在这里加上原文链接: http://www.cnblogs.com/skabyy/p/7695258.html 本篇将实现登录.权限控制.日志配置与审计日志的功能.首先我 ...

  3. ABP(现代ASP.NET样板开发框架)系列之12、ABP领域层——工作单元(Unit Of work)

    点这里进入ABP系列文章总目录 基于DDD的现代ASP.NET开发框架--ABP系列之12.ABP领域层——工作单元(Unit Of work) ABP是“ASP.NET Boilerplate Pr ...

  4. ABP领域层——工作单元(Unit Of work)

    ABP领域层——工作单元(Unit Of work) 点这里进入ABP系列文章总目录 基于DDD的现代ASP.NET开发框架--ABP系列之12.ABP领域层——工作单元(Unit Of work) ...

  5. ABP理论学习之工作单元(Unit of Work)

    返回总目录 本篇目录 公共连接和事务管理方法 ABP中的连接和事务管理 仓储类 应用服务 工作单元 工作单元详解 关闭工作单元 非事务的工作单元 工作单元方法调用其它 工作单元作用域 自动保存 IRe ...

  6. Abp之工作单元与事务

    环境:Abp1.2 疑问:没有调用工作单元的SaveChanges方法引起的事务提交时机的问题. 例如:有一个应用服务代码如下: public void CreatePhrase(PhraseCrea ...

  7. ABP框架 - 工作单元

    文档目录 本节内容: 简介 在ABP中管理连接和事务 约定的工作单元 UnitOfWork 特性 IUnitOfWorkManager 工作单元详情 禁用工作单元 非事务性工作单元 工作单元方法调用另 ...

  8. [ABP]浅谈工作单元 在整个 ABP 框架当中的应用

    ABP在其内部实现了工作单元模式,统一地进行事务与连接管理. 其核心就是通过 Castle 的 Dynamic Proxy 进行动态代理,在组件注册的时候进行拦截器注入,拦截到实现了 Unit Of ...

  9. 深入学习Spring框架(四)- 事务管理

    1.什么是事务? 事务(Transaction)是一个操作序列.这些操作要么都做,要么都不做,是一个不可分割的工作单位,是数据库环境中的逻辑工作单位.事务是为了保证数据库的完整性.例如:A给B转账,需 ...

随机推荐

  1. HDU--1060

    Leftmost Digit Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 65536/32768 K (Java/Others) T ...

  2. FtpUtil.java测试 (淘淘商城第三课文件上传)

    首先在common-taotao中创建一个utils包,复制FtpUtil.java到其中.然后如下: @Test public void testFtpUtil() throws Exception ...

  3. [国嵌攻略][153][I2C裸机驱动设计]

    eeprom简介 eeprom电可擦除可编程只读存储器,是一种类似于flash的固态存储器,但是与flash相比又存在一些区别: 1.eeprom可以按位擦写,而flash只能大片擦除. 2.eepr ...

  4. 滑稽的下午--angularjs 2.0管道的使用

    虽然angular 已经迎来4.0时代,可我还在苦逼的看2.0. 下午有个任务: 让一个component组件里的时间显示当前时间并自动刷新. 过程: 1.首先获取当前时间 new Date(); 2 ...

  5. Java中泛型数组创建总结

    在java中,可以声明一个泛型数组,不能通过直接通过T[] tarr=new T[10]的方式来创建数组,最简单的方式便是通过Array.newInstance(Classtype,int size) ...

  6. LED服务总结

    简单的程序总结 一个简单的用于控制LED屏幕的小程序,用到的一个常识 LED服务开发总结 系统运行截图   系统功能说明: 1.ServerStrack服务,提供前台访问. 2.动态库调用,用于信息转 ...

  7. Go语言是我见过最简洁的语言(除了lua)

    写在前面:题目就是个标题党,在这里先道歉,其次撸主学过很多语言(基本上是个语言都要上一下的那种人,但是不会太深入,只做了解,因为很多用不到),但主要使用C#语言(不过已经开始恶心C#的臃肿,不要打我) ...

  8. 04 整合IDEA+Maven+SSM框架的高并发的商品秒杀项目之高并发优化

    Github:https://github.com/nnngu 项目源代码:https://github.com/nnngu/nguSeckill 关于并发 并发性上不去是因为当多个线程同时访问一行数 ...

  9. ASP.net core 2.0.0 中 asp.net identity 2.0.0 的基本使用(二)—启用用户管理

    修改和启用默认的用户账户管理和角色管理 一.修改Models目录中的ApplicationUser.cs类文件,如下 namespace xxxx.Models{    //将应用程序用户的属性添加到 ...

  10. CentOS7.3 ARM虚拟机扩容系统磁盘

    由于扩容磁盘的操作非同小可,一旦哪一步出现问题,就会导致分区损坏,数据丢失等一系列严重的问题,因此建议:在进行虚拟机分区扩容之前,一定要备份重要数据文件,并且先在测试机上验证以下步骤,再应用于您的生产 ...