Generic Repository&UnitOfWork基本实现
前言
在DbContext中已经具备了事务,对于多个实体的操作,能够在一个事务中保证。借助仓储在基于DbContext上的封装,我们能够更好的扩展复用。泛型仓储的使用又能简化对于基础功能的依赖,但是当现有事务范围不足以覆盖或是多个仓储操作,多次调用SaveChange后,整体的事务范围便发生了变化,不能形成一个整体了。
事务范围
单次SaveChange
新增Order及OrderItem,两者本身都没问题的话,一次SaveChange可以全部成功。而当SaveChange完毕后,再出现异常(如下直接抛出异常test),已有的事务已经提交完毕,无法回滚之前的操作了。
[HttpPost]
public async Task<int> CreateAsync(string name)
{
var order = new Order()
{
Name = name,
CreateDate = DateTime.UtcNow,
OrderItems = new List<OrderItem>()
{
new OrderItem()
{
ProductId = 1,
Amount = 1,
}
}
};
order = await _orderRepository.InsertAsync(order, autoSave: true);
//throw new Exception("test");
return order.Id;
}
多次SaveChange
类似于直接抛出事务,实现多次调用SaveChange,
第一次执行正常,执行SaveChange后数据入库,但在第二次操作时出现失败,第二次的入库操作能够回滚,但是第一次的SaveChange却是还是执行成功,从整个操作上,缺失了一致性。
public async Task<int> CreateMultiAsync(string name)
{
var order = new Order()
{
Name = name,
CreateDate = DateTime.UtcNow,
OrderItems = new List<OrderItem>()
{
new OrderItem()
{
ProductId = 1,
Amount = 1,
}
}
};
order = await _orderRepository.InsertAsync(order, autoSave: true);
var orderError = new Order()
{
Name = name,
CreateDate = DateTime.Now,//数据库中限制只允许utc
OrderItems = new List<OrderItem>()
{
new OrderItem()
{
ProductId = 1,
Amount = 1,
}
}
};
orderError = await _orderRepository.InsertAsync(orderError, autoSave: true);
return order.Id;
}
这种场景在结合领域事件,在handler中还执行一些仓储操作时很容易出现,当没有一个全局的事务保证下,不可避免有这种风险。
全局事务
事务延伸
EFCore事务机制中,可以通过DbContext开启一个事务,然后将所有数据库操作纳入其中。对于单次或是多次SaveChange,便可以合并成一个事务,形成一个整体。
第一次执行SaveChange操作,虽然插入到库,但并未实际提交,不会在表中出现。而当第二次SaveChange如成功,则执行Commit, 两个操作顺利入库。如果发生异常,则第一次操作便是被回滚。
public async Task<int> CreateMultiAsync(string name)
{
var dbContext = await _orderRepository.GetDbContextAsync();
using (var transition = dbContext.Database.BeginTransaction())
{
var order = new Order()
{
Name = name,
CreateDate = DateTime.UtcNow,
OrderItems = new List<OrderItem>()
{
new OrderItem()
{
ProductId = 1,
Amount = 1,
}
}
};
order = await _orderRepository.InsertAsync(order, autoSave: true);
var orderError = new Order()
{
Name = name,
CreateDate = DateTime.Now,//数据库中限制只允许utc
OrderItems = new List<OrderItem>()
{
new OrderItem()
{
ProductId = 1,
Amount = 1,
}
}
};
orderError = await _orderRepository.InsertAsync(orderError, autoSave: true);
transition.Commit();
return order.Id;
}
}
如此一来,事务的范围便延伸起来,但麻烦总是接踵而至,想要一个统一的机制来写事务的开启,提交,而不像如上一样,需要时候写一节代码;并且当多个服务协作时,代码上还是无法形成一个整体的事务,总会充斥着各种嵌套的、平级的调用顺序。
ServiceX.Create()
{
using (var transition = dbContext.Database.BeginTransaction())
{
// do something
ServiceA.Create();
ServiceB.Create();
transition.Commit();
}
}
ServiceA.Create()
{
using (var transition = dbContext.Database.BeginTransaction())
{
// do something
transition.Commit();
}
}
ServiceB.Create()
{
using (var transition = dbContext.Database.BeginTransaction())
{
// do something
transition.Commit();
}
}
工作单元
每次从仓储中取得DbContext开启事务,稍显麻烦,封装一层,加入工作单元概念,由它来管理事务的开启与提交。
Unit Of Work:维护受业务事务影响的对象列表,并协调变化的写入和并发问题的解决。即管理对象的CRUD操作,以及相应的事务与并发问题等。是用来解决领域模型存储和变更工作,而这些数据层业务并不属于领域模型本身具有的。
从其定义来看,DbContext本身便已是实现了Uow,只是当我们需要将其扩展到一个业务用例时,需要协调多个仓储,多个聚合根等,其工作单元的范围也不再局限于DbContext的一次操作了,而是要扩展到整个用例的范围。
IUnitOfWork定义
首先将dbContext的获取与事务提交的职责移交到IUnitOfWork中,定义如下抽象接口
public interface IUnitOfWork : IDisposable
{
IDbContextTransaction BeginTransaction();
void Commit();
}c
借助于依赖注入与DbContext的生命周期,UnitOfWork和Repository能够共用DbContext,如此依赖,代码能够简化
_unitOfWork.BeginTransaction();
var order = new Order();
order = await _orderRepository.InsertAsync(order, autoSave: true);
_unitOfWork.Commit();
再进一步考虑嵌套事务,如果被嵌套的事务中提前进行了Commit,那么导致后续的操作则会报错,因此被嵌套的事务要么不能写,要么要有方法使其知道是被嵌套中,然后令其失效。
ServiceX.Create()
{
_unitOfWorkOuter.BeginTransaction();
// do something
ServiceA.Create();
ServiceB.Create();
_unitOfWorkOuter.Commit();
}
ServiceA.Create
{
_unitOfWorkInner.BeginTransaction();
// do something
_unitOfWorkInner.BeginTransaction();
}
ServiceB.Create
{
_unitOfWorkInner.BeginTransaction();
// do something
_unitOfWorkInner.BeginTransaction();
}
IUnitOfWorkManager定义
为了使其能够区分被嵌套的事务,需要能够在外层事务开启后,再注入内层事务时,能够外层事务已经存在,或许可以再包一层,来保存外层事务,增加一个IUnitOfWorkManager(参照ABP中代码实现)。
public interface IUnitOfWorkManager
{
IUnitOfWork Current { get; }
IUnitOfWork Begin();
}
为了实现内层事务,新增IUnitOfWorkInner,增加空实现
public class UnitOfWorkInner : IUnitOfWork
{
private readonly IUnitOfWork _unitOfWorkOuter;
public UnitOfWorkInner(IUnitOfWork unitOfWorkOuter)
{
_unitOfWorkOuter = unitOfWorkOuter;
}
public IDbContextTransaction BeginTransaction()
{
return _unitOfWorkOuter.BeginTransaction();
}
public void Commit()
{
}
public void Dispose()
{
}
}
在UnitOfWorkManager中,根据是否存在外层事务来决定内存事务的实例。
public class UnitOfWorkManager : IUnitOfWorkManager
{
private readonly IUnitOfWork _unitOfWork;
public UnitOfWorkManager(IUnitOfWork unitOfWork)
{
_unitOfWork = unitOfWork;
}
public IUnitOfWork Begin()
{
var outerUow = Current;
if (outerUow != null)
{
return new UnitOfWorkInner(outerUow);
}
var uow = _unitOfWork;
uow.BeginTransaction();
Current = uow;
return uow;
}
public IUnitOfWork Current
{
get { return GetCurrentUow(); }
set { SetCurrentUow(value); }
}
private static readonly AsyncLocal<LocalUowWrapper> AsyncLocalUow = new AsyncLocal<LocalUowWrapper>();
private static IUnitOfWork GetCurrentUow()
{
var uow = AsyncLocalUow.Value?.UnitOfWork;
if (uow == null)
{
return null;
}
return uow;
}
private static void SetCurrentUow(IUnitOfWork value)
{
lock (AsyncLocalUow)
{
if (AsyncLocalUow.Value?.UnitOfWork == null)
{
if (AsyncLocalUow.Value != null)
{
AsyncLocalUow.Value.UnitOfWork = value;
}
AsyncLocalUow.Value = new LocalUowWrapper(value);
return;
}
AsyncLocalUow.Value.UnitOfWork = value;
}
}
private class LocalUowWrapper
{
public IUnitOfWork UnitOfWork { get; set; }
public LocalUowWrapper(IUnitOfWork unitOfWork)
{
UnitOfWork = unitOfWork;
}
}
}
当再次使用到嵌套事务时候,便可以多次Begin得到外层事务与内层事务,内层事务不具备实际提交功能。
var unitOfWorkOuter = _unitOfWorkManager.Begin();
var order = new Order();
order = await _orderRepository.InsertAsync(order, autoSave: true);
var unitOfWorkInner = _unitOfWorkManager.Begin();
var orderLog = new OrderLog();
await _orderlOGRepository.InsertAsync(orderLog, autoSave: true);
unitOfWorkInner.Commit();
unitOfWorkOuter.Commit();
尽管能够解决嵌套事务,然而还能够简化一些工作量,在DDD中,应用层有承担着事务的开启提交职责,借助于AOP,可以将该部分职责在请求用例时自动启动,也就减少了手动unitOfWorkManager的使用。又或是,Get请求不需要开启事务,可以在AOP中判断Get请求时,不开启事务。
参考资料
- https://learn.microsoft.com/zh-cn/ef/core/saving/transactions
- https://cloud.tencent.com/developer/article/1505237
2023-12-29,望技术有成后能回来看见自己的脚步。