手工搭建基于ABP的框架 - 工作单元以及事务管理
一个业务功能往往不只由一次数据库请求(或者服务调用)实现。为了功能的完整性,我们希望如果该功能执行一半时出错,则撤销前面已执行的改动。在数据库层面上,事务管理实现了这种完整性需求。在ABP中,一个完整的业务功能称为一个工作单元(Unit of Work,简称UoW)。工作单元代表一种完整的、原子性的操作。即一个工作单元包含的步骤要么全部被执行,要么都不被执行。如果执行一半时出现异常,则必须讲已执行的步骤还原。通常我们将事务管理实现在工作单元中。下面我们从ABP源码入手研究如何使用工作单元。
ABP工作单元(UoW)的工作原理
ABP默认将工作单元应用在Repositories、 Application Services、MVC控制器和Web API控制器等组件。也就是说,这些组件的每个方法都是一个工作单元。ABP文档对工作单元的原理讲得不是很详细,所以我们只能通过源码进行研究。这里我们以MVC控制器为例来了解一下ABP工作单元大致的工作原理。源码分析比较枯燥,最好配套ABP源码阅读,或者跳到后面看粗体字结论。
ABP在Web模块初始化时注册了过滤器AbpMvcUowFilter
。AbpMvcUowFilter
在请求处理前(OnActionExecuting
方法)调用UnitOfWorkManager.Begin
方法来开始一个工作单元。UnitOfWorkManager.Begin
创建一个IUnitOfWork
的实例并赋值给ICurrentUnitOfWorkProvider.Current
,然后调用IUnitOfWork.Begin
方法开始一个工作单元。在请求处理结束后(OnActionExecuted
方法)如果处理过程没有异常就调用IUnitOfWork.Complete
方法完成工作单元,并且无论请求处理是否成功,都调用IUnitOfWork.Dispose
来结束工作单元。
ABP提供了一个实现IUnitOfWork
的抽象基类UnitOfWorkBase
,另外还有个继承了UnitOfWorkBase
的类NullUnitOfWork
。NullUnitOfWork
定义上面有一段注释如此写到:
/// <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
方法。我们需要重写的主要是BeginUow
、CompleteUow
和DisposeUow
这三个方法。
通过源码简单了解了原理后,我们后面写代码要注意的有下面几点:
- 写一个继承
UnitOfWorkBase
的类UnitOfWork
,并实现接口ITransientDependency
保证UnitOfWork
被注册到IoC容器; - 重写方法
UnitOfWorkBase.BeginUow
,实现工作单元开始时的启动操作; - 重写方法
UnitOfWorkBase.CompleteUow
,实现工作单元正常结束时的保存操作; - 重写方法
UnitOfWorkBase.DisposeUow
,实现工作单元结束时的清理操作; - 通过
ICurrentUnitOfWorkProvider.Current
来获取当前的工作单元。
重写SessionProvider,并实现工作单元
在之前文章(手工搭建基于ABP的框架(2) - 访问数据库)实现的LocalDbSessionProvider
中,为了追求代码简单,我们粗暴地用一个实质上是全局的变量来保存数据库Session,在每次访问数据库时,flush上一个Session并创建新Session。另一方面,数据库连接的配置、Session的创建保存、以及Session的提供都胡乱地放在了这个类里。这其实是非常不合理而且会引发很多问题的实现方法。
下面我们重新设计这一块逻辑。我们将LocalDbSessionProvider
所负责的功能拆分,分别实现在LocalDbSessionConfiguration
、UnitOfWork
和UnitOfWorkLocalDbSessionProvider
三个类中:
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(); }
最后,TweetRepository
和TweetQueryService
的构造函数用到了旧的LocalDbSessionProvider
,这两处也需要改一下:
public TweetRepository() : base(IocManager.Instance.Resolve<UnitOfWorkLocalDbSessionProvider>()) { }
public TweetQueryService() : base(IocManager.Instance.Resolve<UnitOfWorkLocalDbSessionProvider>()) { }
使用NHProfiler进行验证
上面实现了工作单元并封装了数据库事务管理。我们需要有方法验证数据库访问时确实开启了事务。NHProfiler是一个能够监视NHibernate生成的SQL语句的工具。我们将使用NHProfiler查看生成的SQL,确认实现了工作单元后确实开启了事务管理。
NHProfiler由两个部分组成:
- 一个嵌入到我们应用的DLL。这个DLL会在NHibernate访问数据库时往本地socket发送生成的SQL语句。
- 客户端。这个客户端通过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
实现工作单元,使得整个框架更灵活,更容易扩展。在现有代码上稍作修改,我们还可以支持多数据库事务。