abp学习笔记 - 工作单元(UOW)基础
简介
最近在梳理项目系统架构,发现团队虽然使用了abp,但实际还是三层架构那套思想,尤其是对于dbcontext的滥用,加上对UOW(unit of work)的不了解,导致了很多奇葩写法。
而我也对ABP一知半解,导致codereview并没有发现异常。故此来系统化的学习并分享自己的踩坑经验。
工作单元基础
我现在写的代码,跑在哪个工作单元里?它会在什么时候提交
工作单元是什么
工作单元(UOW)是一次业务操作边界,它同时管理四件事情:
- 数据库上下文
整个业务操作共用同一个DbContext实例 - 事务
可选,官方明确说"UOW"不一定是事务 - 保存时机
默认在业务操作边界结束时SaveChanges - 提交后的回调
OnCompleted在成功提交后执行
简单来说,UOW = 一个 DbContext + 可选事务 + 统一保存和提交的时机。
工作单元怎么开启
下面四种方法就会默认开启
- Controller的Action
- Razor Page的Handler
- Application Service的方法
- Repository 的方法
默认开启的前提是:外部还没有一个
举个例子,一个接口从Action=>Service=>Repository,如果都开启UOW就会有3个,这样就乱套了。实际上如果外部已经有一个UOW,默认情况下会并入同一个UOW。
HTTP方法决定是否带事务
| 请求 | UoW | 数据库事务 |
|---|---|---|
GET(以及 QUERY) |
开 | 不开 |
POST / PUT / DELETE |
开 | 开(数据库支持的话) |
默认值由AbpUnitOfWorkDefaultOptions控制,默认是Auto。
如果是手动开启UOW,是不是事务由isTransactional决定
眼见为实
///
/// 论断:不在任何工作单元里时,IUnitOfWorkManager.Current 是 null。
/// 所以要么是WEB接口,否则要自己开。
///
[Fact]
public async Task Lesson01_OutsideAnyUnitOfWork_ThereIsNoCurrentOne()
{
GetRequiredService().Current.ShouldBeNull();
await Task.CompletedTask;
}
踩坑记录:如果你的系统中有消费Message Queue等非WEB场景,是不会开启UOW,需要自己手动开启
UOW与事务的关系
很多人都容易弄混淆,也包括我。都认为UOW = Transaction。
实际上,UOW > Transaction,是更健壮的实现。
UOW的作用不止是Transaction,Transaction只是它的一个可选项。关掉事务后,它仍然负责下面这几件事
| 作用 | 说明 |
|---|---|
| 共用一个DbContext | 同一个UOW里拿到的都是同一个DbContext,实体只被跟踪一份,互相可以看到改动 |
| 延迟保存,批量写入 | 改动先留在内存里,UOW结束时统一SaveChanges,只更新有变化的内容 |
| 管理DbContext的生命周期 | UOW开始时创建,结束时释放,没有它,GetQueryableAsync返回的查询会报错 |
| 统一的边界 | OnCompleted只在成功完成后执行,我们用它来做消息发布,失败也有统一的出口 |
非事务UOW少了什么
少了原子性和回滚
- 事务UOW
中途失败,autoSave写入的行也会随事务回滚 - 非事务UOW
每次保存各自生效,多次保存独立生效。
如果是使用EF,那么一次SaveChanges即使在非事务UOW里也是原子的,因为这是EF SaveChanges的通用行为
为什么官方允许不要事务
- GET查询不需要
只读接口开事务只会白白占用连接,浪费资源 - 事务消耗大
事务会占住数据库连接,持有快照与锁,如果事务里还有HTTP,GPRC请求,调用耗时会被事务放大 - 有些流程本来就需要“逐步独立提交”
并不是所有流程都需要原子提交,很多时候我们要立即保存,失败就跳过。如果套一个事务,反而添麻烦。
眼见为实
///
/// 论断:在工作单元之外调用 GetQueryableAsync,拿到的查询无法执行(ObjectDisposedException)。
/// 原因:GetQueryableAsync 自己开的工作单元在返回时就结束了,而查询要到 ToListAsync 才真正执行,
/// 那时它绑定的 DbContext 已经随工作单元释放。
/// 规则:“取查询 → 执行查询”必须在同一个工作单元里;或者把查询封装进自定义仓储方法。
///
[Fact]
public async Task Lesson02_GetQueryableAsync_OutsideAUnitOfWork_ReturnsAQueryThatCannotRun()
{
var repository = GetRequiredService>();
var query = await repository.GetQueryableAsync();
await Should.ThrowAsync(async () => await query.ToListAsync());
}
///
/// 论断:在【非事务】工作单元里,autoSave: true 的写入是立刻生效的,即使之后没有调用 CompleteAsync 也不会撤销。
/// 意图:纠正“没 Complete 就等于回滚”的想象——那只在事务工作单元里成立。
/// 非事务工作单元里每次 SaveChanges 各自生效,多次保存之间没有原子性。
///
[Fact]
public async Task Lesson02_AutoSaveInsideANonTransactionalUnitOfWork_IsKeptEvenWithoutComplete()
{
var manager = GetRequiredService();
var repository = GetRequiredService>();
using (manager.Begin())
{
await repository.InsertAsync(NewResult(), autoSave: true);
// 故意不调用 CompleteAsync
}
(await CountAsync()).ShouldBe(1, "非事务工作单元:autoSave 写入后没有可回滚的事务,行已经留下了");
}
///
/// 论断:工作单元不一定是事务,是不是事务由 isTransactional 决定:
/// - 手动 Begin() 默认【不是】事务;Begin(isTransactional: true) 才是;
///
[Fact]
public async Task Lesson01_UnitOfWork_IsOnlyATransactionWhenItIsToldToBe()
{
var manager = GetRequiredService();
using (var plain = manager.Begin())
{
plain.Options.IsTransactional.ShouldBeFalse("手动 Begin() 默认不开事务");
}
using (var transactional = manager.Begin(isTransactional: true))
{
transactional.Options.IsTransactional.ShouldBeTrue();
}
}
环境工作单元(Ambient UOW)
IUnitOfWorkManager.Current底层基于AsyncLocal,因此同一个调用链路上,嵌套的方法看到的是同一个Current。
这也解释了,官方为什么说,仓储不需要知道UOW跟事务,同一个请求先后调用的多个Service,Repository,用的是同一个DbContext

手动开启UOW与UOW嵌套:Begin()与Begin(requiresNew: true)
当我们需要手动开启一个工作单元时,可以选择复用外层的UOW,还是创建一个全新的UOW,这取决于你的实际需要。
眼见为实
///
/// 论断:已有工作单元时,Begin() 默认“加入”它(共用同一个 Id、DbContext 和事务);
/// 只有 requiresNew: true 才会另开一个独立的。
///
[Fact]
public async Task Lesson01_NestedBegin_JoinsTheAmbientUnitOfWork_UnlessRequiresNew()
{
var manager = GetRequiredService();
using var outer = manager.Begin();
// 默认:加入外层,Id 相同
// Begin() 返回的 Id 等于外层的 Id → 它是外层的子包装,共用同一个 DbContext 和事务
using (var joined = manager.Begin())
{
joined.Id.ShouldBe(outer.Id);
}
// requiresNew:忽略外层,新开一个,Id 不同
// Begin(requiresNew: true) 的 Id 不同 → 完全独立,有自己的 DbContext 和事务。
using (var separate = manager.Begin(requiresNew: true))
{
separate.Id.ShouldNotBe(outer.Id);
}
await outer.CompleteAsync();
}
记住一个法则,只有当你确实需要独立提交,独立回滚时才用requiresNew: true,并且你要清楚的知道,它看不到外层未提交的数据
更优雅的选择,【UnitOfWork】特性
从上面的例子中可以看到,手动创建UOW并不优雅,因为官方提供了特性来提高代码可读性。
public class LessonUowProbe : Volo.Abp.DependencyInjection.ITransientDependency
{
private readonly IUnitOfWorkManager _unitOfWorkManager;
public LessonUowProbe(IUnitOfWorkManager unitOfWorkManager) => _unitOfWorkManager = unitOfWorkManager;
[UnitOfWork(IsDisabled =true)]
public virtual Task VirtualMethod() => Task.FromResult(_unitOfWorkManager.Current != null);
[UnitOfWork(IsDisabled = true)]
public Task NonVirtualMethod() => Task.FromResult(_unitOfWorkManager.Current != null);
}
但使用特性有四条规则需要注意
- 当类不是通过接口注入时,被标记的方法必须是virtual,否则无法拦截
- 只有Async方法会被拦截,同步方法不行
- 方法在已有环境UOW里被调用时,特性会被忽略,直接加入已有的
- 日志的categories会变换
眼见为实
///
/// 论断:[UnitOfWork] 只对 virtual 方法生效;非 virtual 方法上的特性会被静默忽略,没有任何编译错误或警告。
/// 意图:防止给订阅方法加了 [UnitOfWork] 却忘了写 virtual,以为开了工作单元其实没有。
/// 方法:被测类从容器解析,这样 ABP 才会给它生成代理(和 CAP 调用订阅方时一致);用 new 出来的对象没有代理。
///
[Fact]
public async Task Lesson01_UnitOfWorkAttribute_OnlyWorksOnVirtualMethods()
{
var probe = GetRequiredService();
(await probe.VirtualMethod()).ShouldBeTrue("virtual 方法带 [UnitOfWork]:代理会为它开一个工作单元");
(await probe.NonVirtualMethod()).ShouldBeFalse("非 virtual 方法无法被拦截,特性被静默忽略");
}
完成,回滚与回调
- 调用
CompleteAsync(),保存并提交 - 没有调用
CompleteAsync(),就Dispose不提交 OnCompleted(callback)
成功完成后才执行回调。Failed / Disposed 事件分别在失败 / 释放时触发。
眼见为实
///
/// 论断:不在任何工作单元里时,IUnitOfWorkManager.Current 是 null。
/// 所以订阅方里要用工作单元,必须自己开,或者用 [UnitOfWork]。
///
[Fact]
public async Task Lesson01_OutsideAnyUnitOfWork_ThereIsNoCurrentOne()
{
GetRequiredService().Current.ShouldBeNull();
await Task.CompletedTask;
}
///
/// 论断:OnCompleted 只在工作单元“成功完成”之后才执行;没有调用 CompleteAsync 就释放的话不会执行。
/// 意图:解释为什么“提交后再发布消息”应该用 OnCompleted——事务回滚时消息不会发出去。
/// (官方没有写明回调与提交的先后顺序,以及回调抛异常的后果,所以我们的回调自己 try/catch,不依赖这些。)
///
[Fact]
public async Task Lesson01_OnCompleted_RunsAfterCommit_AndNotWhenTheUnitOfWorkIsNotCompleted()
{
var manager = GetRequiredService();
var completedCallbackRan = false;
var abandonedCallbackRan = false;
// 正常完成:回调执行
using (var uow = manager.Begin())
{
uow.OnCompleted(() => { completedCallbackRan = true; return Task.CompletedTask; });
await uow.CompleteAsync();
}
// 没有调用 CompleteAsync 就释放(相当于处理过程中抛了异常):回调不执行
using (var uow = manager.Begin())
{
uow.OnCompleted(() => { abandonedCallbackRan = true; return Task.CompletedTask; });
}
completedCallbackRan.ShouldBeTrue();
abandonedCallbackRan.ShouldBeFalse();
}
小结
| 要点 | 总结 |
|---|---|
| UOW不等于事务 | GET有UOW,但没有事务 |
| 谁开 | 控制器、应用服务、仓储方法;其他默认没有,比如消息队列订阅 |
| [UnitOfWork] | 必须 virtual + async,在已有 UoW 里被忽略 |
| 回调 | OnCompleted只在成功后执行 |
课后小练习
第 1 题 HTTP GET 请求里,有没有 UoW?有没有数据库事务?
答案
有 UoW,没有数据库事务(官方:GET 和 QUERY 是安全方法)。在 GET 里写数据,要么显式开启事务,要么接受每次仓储调用单独提交。
第 2 题 下面这个方法在 UoW 里被调用吗?它的 Current 是什么?
[UnitOfWork]
public Task Handle() => Task.FromResult(_manager.Current != null); // 注意:不是 virtual
答案
不在,Current 为 null。非 virtual 方法无法被拦截,特性被静默忽略(Lesson01_UnitOfWorkAttribute_OnlyWorksOnVirtualMethods)。改成 public virtual,并让调用方从容器解析该类。
第 3 题 外层 using var outer = manager.Begin(); 之后,内层分别写 manager.Begin() 和 manager.Begin(requiresNew: true)。它们和外层是同一个 DbContext 吗?
答案
Begin():是(加入外层,Id 相同);requiresNew: true:不是(另一个 DbContext 和事务,Id 不同)。
原文地址: https://www.cveoy.top/t/topic/qHMP 著作权归作者所有。请勿转载和采集!