abp vnext 工作单元(UnitOfWork)与事务 电脑版发表于:2023/7/19 22:52 tn2>工作单元是一个比较重要的基础设施组件,它负责管理整个业务流程当中涉及到的数据库事务,一旦某个环节出现异常自动进行回滚处理。工作单元嘛,听名字就知道是把一系列操作当作一个整体一个单元,这样可以统一控制它的提交与回滚<br> 在 ABP vNext 框架当中,工作单元被独立出来作为一个单独的模块(Volo.Abp.Uow)。你可以根据自己的需要,来决定是否使用统一工作单元 tn4>ABP框架的工作单元(UOW)实现提供了对应用程序中的数据库连接和事务范围的抽象和控制。<br> 一旦一个新的UOW启动,它将创建一个环境作用域,当前作用域中执行的所有数据库操作都将参与该作用域并将其视为单个事务边界. 操作一起提交(成功时)或回滚(异常时)。 [TOC] #### ABP的UOW系统 - 按约定工作, 所以大部分情况下你不需要处理UOW. - 数据库提供者独立. - Web独立, 这意味着你可以在Web应用程序/服务之外的任何类型的应用程序中创建工作单元作用域. #### 数据库事务行为 UOW是数据库事务,但实际上UOW不必是事务性的. 默认情况下 - HTTP GET请求不会启动事务性UOW. 它们仍然启动UOW,但不创建数据库事务. - 如果底层数据库提供程序支持数据库事务,那么所有其他HTTP请求类型都使用数据库事务启动UOW. 这是因为HTTP GET请求不会(也不应该)在数据库中进行任何更改,但是可以使用下面解释的选项来更改此行为. **示例: 完全禁用数据库事务:** ``` Configure<AbpUnitOfWorkDefaultOptions>(options => { options.TransactionBehavior = UnitOfWorkTransactionBehavior.Disabled; }); ``` **配置选项:** - `TransactionBehavior (enum: UnitOfWorkTransactionBehavior)`. 配置事务行为的全局点. 默认值为 `Auto` , 你可以使用此选项启用(甚至对于HTTP GET请求)或禁用事务. - `TimeOut (int?)`: 用于设置UOW的超时值. 默认值是` null` 并使用基础数据库提供程序的默认值. - `IsolationLevel (IsolationLevel?)`: 如果UOW是事务性的用于设置数据库事务的隔离级别. 所以一般使用默认的配置是比较科学的,我们就不用去动它。 ## 使用默认注入的UOW #### 一个简单的使用默认注入UOW的单表示例: ``` using System.Threading.Tasks; using Volo.Abp.Application.Services; using Volo.Abp.Domain.Repositories; namespace AbpDemo { public class CategoryAppService : ApplicationService, ICategoryAppService { private readonly IRepository<Category, int> _categoryRepository; public CategoryAppService(IRepository<Category, int> categoryRepository) { _categoryRepository = categoryRepository; } public async Task<int> CreateAsync(string name) { var category = new Category {Name = name}; await _categoryRepository.InsertAsync(category); //如果主键是自增Id,调用后就可以返回自增Id await UnitOfWorkManager.Current.SaveChangesAsync(); return category.Id; } } } ``` `IUnitOfWork.SaveChangesAsync()`方法将到目前为止的所有更改保存到数据库中. 如果你正在使用EF Core,它的行为完全相同. 如果当前UOW是事务性的,即使已保存的更改也可以在错误时回滚(对于支持的数据库提供程序). 示例是从基类 `ApplicationService` 派生的应用服务, `IUnitOfWorkManager` 服务已经作为 `UnitOfWorkManager` 属性注入,所以无需手动注入. 获取当前UOW非常常见,所以还有一个`UnitOfWorkManager.Current`的快捷属性 `CurrentUnitOfWork`. 所以可以对上面的例子进行以下更改 ``` await CurrentUnitOfWork.SaveChangesAsync(); ``` 但是如果只是对单表操作,想要马上保持到数据库的情况,其实不需要去手动调用`UnitOfWorkManager.Current.SaveChangesAsync()`设置`autoSave`为ture就行了。示例代码如下: ``` public async Task<int> CreateAsync(string name) { var category = new Category {Name = name}; await _categoryRepository.InsertAsync(category, autoSave: true); return category.Id; } ``` 两个小建议: tn2> 1: 当工作单元结束而没有任何错误时,所有更改都会自动保存,有错误就会自动回滚。所以除非确实需要,否则不要调用 SaveChangesAsync() 和设置 autoSave 为 true。因为这样它指向完毕后不会马上存储,它就可以和其他操作当作一个整起来提交或者回滚,这样通常来说更科学一些,因为通常来说我们希望我们的一次请求做的操作那么是都成功的,那么就是都失败,保证了数据完整性。 <br/> 2: 如果你使用 Guid 作为主键,则无需插入时保存来获取生成的id,因为 Guid 主键是在应用程序中设置的,创建新实体后立即可用. 注意点: tn4>注意:仅异步方法(返回Task或Task<T>的方法)被拦截. 因此同步方法无法启动UOW <br> 注意:异步方法要尽量避免使用void返回值,就是不要写成async void这种写法,就算不要返回值也要写成async task。因为一旦使用了async void这种写法,方法就不能被等待了,调用这个方法的地方就不会去等待这个方法的执行,就容易造成各种对象被释放的问题,常见的就是ef上下文对象在异步情况下被自动释放的问题。参考:https://www.zhihu.com/question/465625163 #### 使用默认注入UOW实现,同时添加两张表,是1对多的关系 实体DTO贴一下,就是一个1对多的关系 ``` public class CreateInquiryInfoDto { public InquiryInfoDto inquiryInfoDto { get; set; } public List<LoadPortChargesDto> loadPortChargesDtos { get; set; } ``` 实现代码: ``` public async Task<bool> AddInquiryAndLoadPortChargesAsync(CreateInquiryInfoDto createInquiryInfoDto) { InquiryInfo inquiryInfo = ObjectMapper.Map<InquiryInfoDto, InquiryInfo>(createInquiryInfoDto.inquiryInfoDto); List<LoadPortCharges> loadPortCharges = ObjectMapper.Map<List<LoadPortChargesDto>, List<LoadPortCharges>>(createInquiryInfoDto.loadPortChargesDtos); InquiryInfo result = await _inquiryInfoRepository.InsertAsync(inquiryInfo); // 先调用一次SaveChangesAsync才能拿到InquiryInfo表返回的自增id的值,如果主键是GUID会自动生成,这里可以不用调用SaveChangesAsync,而且GUID这种我们也可以自己控制,也很方便 await UnitOfWorkManager.Current.SaveChangesAsync(); // 赋值为上面对象返回的id foreach (LoadPortCharges item in loadPortCharges) { item.InquiryInfoId = result.Id; } await _loadPortChargesRepository.InsertManyAsync(loadPortCharges); return result != null; } ``` tn2>非常简单,abp vnext会自动开启工作单元,不用自己去管理,这里操作了多张表的存储,如果执行完成后没有任何错误就会自动存储,如果有错误就会自动回滚,就算是这个方法执行完毕后在api层action方法这种出错也会回滚的哦。所以需要说明的是还不仅仅是这里的方法操作数据库的会提交和回滚,它默认开启的工作单元是一个更大的整体,比如我们请求的api控制器还调用了其他方法,其他方法里边也操作了几张表的更改,也是会一起当作一个工作单元去处理的,非常方便。abp vnext里边默认会把控制器里边的action,应用程序方法,仓储方法都当作一个工作单元去处理。如果你想要更精细或者是更低维度的控制一个工作单元,比如是某一个方法单独的控制一个工作单元,你可以手动开启一个工作单元,或者是使用一个特性标记,后面有介绍。 tn4>abp nvext的工作单元就相当于是在数据库之上对事物操作相关的封装,更高维度、更高一层的封装。但是要注意工作单元并不一定都是事务性的,因为就数据库而言也并不是所有数据库都支持事物的哇。还有一点就是说到的有错误就会回滚,但是要注意一下如果你执行的方法里边自己加了异常处理,保证了即使出现了异常请求的方法和里边调用的方法都能够顺利执行完成,而你异常处理里边又没有执行回滚的话,它会任务这个工作单元是正常执行的哦,它后面也会提交的哦,所以异常处理建议放到全局去处理,这样就不会影响了单独的方法逻辑了,建议把异常处理放到过滤器中实现以下IAsyncExceptionFilter或者IExceptionFilter即可,如果想要在方法里边单独的去处理异常,那么建议去手动开始工作单元。 ## 手动开启事务 在某些情况下你可能希望更改常规事务作用域,创建内部作用域或精细控制事务行为,可以手动开启。 #### 除了使用默认注入的工作单元(UnitOfWork),也可以自己注入一个,示例代码如下: ``` using System.Threading.Tasks; using Volo.Abp.DependencyInjection; using Volo.Abp.Uow; namespace AbpDemo { public class MyService : ITransientDependency { private readonly IUnitOfWorkManager _unitOfWorkManager; public MyService(IUnitOfWorkManager unitOfWorkManager) { _unitOfWorkManager = unitOfWorkManager; } public virtual async Task FooAsync() { using (var uow = _unitOfWorkManager.Begin( requiresNew: true, isTransactional: true )) { //你的各种操作 await uow.CompleteAsync(); } } } } ``` 当然一般都很少这么做,因为我们的领域服务都是继承ApplicationService了的,默认就已经被工作单元注入了,直接使用即可。前面的和下面的都有使用,这种方式只是介绍一下即可。 **Begin 方法有以下可选参数:** - requiresNew (bool): 设置为 true 可忽略周围的工作单元,并使用提供的选项启动新的UOW. 默认值为false. 如果为false,并且周围有UOW,则 Begin 方法实际上不会开始新的UOW,而是以静默方式参与现有的UOW. - isTransactional (bool). 默认为 false. - isolationLevel (IsolationLevel?): 如果UOW是事务的,用于设置数据库事务的隔离级别. 如果未设置,则使用默认值. TimeOut (int?): 用于设置UOW的超时值. **默认值为 null**并回退到默认配置值. #### 上面是使用依赖注入的方式获取的工作单元类,其实我们一般都会继承ApplicationService,会自动把工作单元的类注入进来,就不需要手动去通过注入获取了 示例代码如下,直接通过UnitOfWorkManager就可以获取当前的当前工作单元了,点击调用begin就可以开启使用了非常方便。 ``` public virtual async Task FooAsync() { AbpUnitOfWorkOptions abpUnitOfWorkOptions = new AbpUnitOfWorkOptions(); // 设置为是事务性的工作单元 abpUnitOfWorkOptions.IsTransactional = true; using (var uow = UnitOfWorkManager.Begin(abpUnitOfWorkOptions, requiresNew: true)) { // 操作完成之后,我们需要调用 Complete() 方法来说明我们的操作已经完成了。如果你没有调用 Complete() 方法,那么工作单元在被释放的时候,就会产生异常,并触发 Failed 事件。 await uow.CompleteAsync(); } } ``` ### 使用示例,同时添加两张表,是1对多的关系 实体DTO和上面使用默认提交的工作单元一样 实现代码: ``` public async Task<bool> AddInquiryAndLoadPortChargesAsync(CreateInquiryInfoDto createInquiryInfoDto) { InquiryInfo inquiryInfo = ObjectMapper.Map<InquiryInfoDto, InquiryInfo>(createInquiryInfoDto.inquiryInfoDto); List<LoadPortCharges> loadPortCharges = ObjectMapper.Map<List<LoadPortChargesDto>, List<LoadPortCharges>>(createInquiryInfoDto.loadPortChargesDtos); AbpUnitOfWorkOptions abpUnitOfWorkOptions = new AbpUnitOfWorkOptions(); // 设置为是事务性的工作单元 abpUnitOfWorkOptions.IsTransactional = true; // 事务隔离级别的设置 //abpUnitOfWorkOptions.IsolationLevel = System.Data.IsolationLevel.Serializable; // 使用工作单元开启使用,如果出错会销毁回滚 using (var uow = UnitOfWorkManager.Begin(abpUnitOfWorkOptions, requiresNew: true)) { InquiryInfo result = await _inquiryInfoRepository.InsertAsync(inquiryInfo); // 先调用一次SaveChangesAsync才能拿到InquiryInfo表返回的自增id的值,没有调用CompleteAsync前数据库是查询不到的。如果主键是GUID会自动生成,这里可以不用调用SaveChangesAsync await uow.SaveChangesAsync(); // 赋值为上面对象返回的id foreach (LoadPortCharges item in loadPortCharges) { item.InquiryInfoId = result.Id; } await _loadPortChargesRepository.InsertManyAsync(loadPortCharges); // 操作完成之后,我们需要调用 Complete() 方法来说明我们的操作已经完成了。如果你没有调用 Complete() 方法,那么工作单元在被释放的时候,就会产生异常,并触发 Failed 事件。 await uow.CompleteAsync(); return result != null; } } ``` 这种写法是手动开始事务的,写起来要麻烦点,但是自己的控制性更强一点,如果要想用简化的写法一般使用自带的工作单元就行了,参考上面的:<a href="https://www.tnblog.net/aojiancc2/article/details/8161#使用默认注入UOW实现,同时添加两张表,是1对多的关系">使用默认注入UOW实现,同时添加两张表,是1对多的关系</a> 像上面这种一个操作会依赖另外一个操作的结果,建议把事务隔离级别设置得比较高一些,防止并发问题,mysql事务详解参考:https://www.tnblog.net/aojiancc2/article/details/7264 为什么工作单元常常配合 using 语句块 使用,就是因为在提交工作单元之后,就可以自动调用 Dispose() 方法,对工作单元的状态进行校验,而不需要我们手动处理。 上面的方法在提交前任意地方出错都会回滚,可以自行测试。 ## 当然还可以使用特性的方式 **使用UnitOfWork特性即可,可以放到具体方法上** (tip:没有继承IUnitOfWorkEnabled类不会有默认的UOW对象,像这种把工作单元的特性放到一个方法上面就只会把这一个方法当作一个工作单元,可以更精细化的控制) ``` using System.Threading.Tasks; using Volo.Abp.DependencyInjection; using Volo.Abp.Uow; namespace AbpDemo { public class MyService : ITransientDependency { [UnitOfWork] public virtual async Task FooAsync() { //this is a method with a UOW scope } public virtual async Task BarAsync() { //this is a method without UOW } } } ``` 当我们在调用这个被注解的方法时,ABP 框架会自动创建一个新的工作单元对象,并在其作用域内执行标记了 [UnitOfWork] 特性的部分代码。如果这些代码全部执行成功,则工作单元会自动提交事务;否则,工作单元会自动回滚事务,以保证数据的完整性。但是要注意如果这个方法本身已经是一个工作单元范围内的方法了,那么加不加这个特性都没有什么影响,执行完成这个方法工作单元也不一定会马上去提交,因为要看当前工作单元的范围,因为还很有可能其他方法也是在这个工作单元范围内的,abp nvext默认工作单元的范围参考上面介绍的。 **也可以放到类上面:** ``` using System.Threading.Tasks; using Volo.Abp.DependencyInjection; using Volo.Abp.Uow; namespace AbpDemo { [UnitOfWork] public class MyService : ITransientDependency { public virtual async Task FooAsync() { //this is a method with a UOW scope } public virtual async Task BarAsync() { //this is a method with a UOW scope } } } ``` **继承`IUnitOfWorkEnabled`接口也可以** ``` using System.Threading.Tasks; using Volo.Abp.DependencyInjection; using Volo.Abp.Uow; namespace AbpDemo { public class MyService : ITransientDependency, IUnitOfWorkEnabled { public virtual async Task FooAsync() { //this is a method with a UOW scope } } } ``` 其实上面说的继承`ApplicationService`类就可以使用UOW其实就是因为`ApplicationService`类引用了的`IUnitOfWorkEnabled`的 abp vnext会自动开启工作单元,所以大多数情况下,使用默认开启的UOW都够用了,而且很方便,不用自己去管理。