如何解决 Dapper - UnitOfWork 事务错误

Posted

技术标签:

【中文标题】如何解决 Dapper - UnitOfWork 事务错误【英文标题】:How to solve Dapper - UnitOfWork Transaction Error 【发布时间】:2021-10-13 00:21:55 【问题描述】:

我正在尝试在 ASP.NET Core Web API 中使用 Dapper 实现工作单元存储库模式。

我已经创建了模型、存储库和 UOW。当我尝试执行 GET 请求时出现错误

System.InvalidOperationException: BeginExecuteReader 需要 命令在连接分配给 命令处于待处理的本地事务中。交易属性 的命令尚未初始化。

这是我的控制器;

 public class CityController : ControllerBase
    
        private readonly IUnitOfWork unitOfWork;
        public CityController(IUnitOfWork unitOfWork)
        
            this.unitOfWork = unitOfWork;
        
        [HttpGet]
        public async Task<IEnumerable<City>> GetAll()
        
            return await unitOfWork.Cities.All();
        

CityRepository.cs

internal class CityRepository : GenericRepository<City>, ICityRepository
    
       public CityRepository(IDbTransaction transaction)
            : base(transaction)
        

        
        public async Task<IEnumerable<City>> All()
        

            var model = await Connection.QueryAsync<City>("SELECT * FROM DT_Inspection.City");
            return model.ToList();

        

public IConfiguration configuration;
        private IDbTransaction _transaction;
        private IDbConnection _connection;
        ICityRepository _cityRepository;
        private bool _disposed;

        public UnitOfWork(IConfiguration _configuration)
        
            configuration = _configuration;
            _connection = new SqlConnection(_configuration.GetConnectionString("DefaultConnection"));
            _connection.Open();
            _transaction = _connection.BeginTransaction();          
        
        public ICityRepository Cities  get  return _cityRepository ?? (_cityRepository = new CityRepository(_transaction)); 
public void Commit()
        
            try
            
                _transaction.Commit();
            
            catch
            
                _transaction.Rollback();
                throw;
            
            finally
            
                _transaction.Dispose();
                _transaction = _connection.BeginTransaction();
                resetRepositories();
            
        

【问题讨论】:

在尝试使用“存储库”之前尝试编写正确 ADO.NET 代码。正如错误所说,您需要将 DbCommand 与事务显式链接。或者您可以使用 TransactionScope。 Dapper 是 ADO.NET 之上的一个非常薄的映射层。您仍然需要知道如何使用 ADO.NET。顺便说一句,您尝试创建的实际上是一个数据访问对象,使用悲观(即不是非常可扩展)并发。不是一个真正的存储库,它使持久性看起来像内存中的操作。 您尝试做的事情显式地阻止了 ADO.NET 通过 DataTables 或 EF 的 DbContext 支持的更具可扩展性 (100-1000x) 的乐观并发。如果您使用显式事务,您陷入死锁,因此您需要为它们做好准备。使用事务的简单方法(无论您是否将它们包装在 UoW 中)是在打开连接之前创建一个TransactionScope。新连接将自动加入事务 否则需要将数据库事务传递给每个命令。在 Dapper 的情况下,这意味着将其作为 QueryExecutetransaction 参数传递。 Dapper 不会绕过 ADO.NET,它会为您传递给它的 SQL 字符串创建和缓存 DbCommand 实例。不用说,“存储库”实现开始泄漏,因为CityRepository 类需要访问存储在UoW 中的_transaction 字段 顺便说一句,UoW 必须是一次性的,它必须处理它持有的连接和事务。两者都应该是短暂的_transaction = _connection.BeginTransaction(); 违反了工作单元模式。您所拥有的不是表示工作单元,而是使用隐式事务的泄露的持久连接,如果您使用特定的连接字符串设置,Oracle 或 SQL Server 就会这样做。但是很少有人使用它们,因为它们会使死锁和阻塞变得更糟 【参考方案1】:

对于初学者来说,这与工作单元完全相反。工作单元意味着你有一个单一的、不可分割的一组(unit)操作(work)需要作为一个提交或丢弃。一旦完成,它就消失了,无法重复使用。这是一个功能。

UoW 通常意味着该工作在提交之前不会影响数据源,但是您的代码会启动一个昂贵的长期事务,该事务确实从第一次读取开始就锁定记录。

您使用的类创建了一个 global 长期连接和一个全局的隐式事务。这是一个非常糟糕的做法。这些行特别是一个主要错误:

_transaction = _connection.BeginTransaction();
resetRepositories();

您可以通过一些连接设置在任何数据库中实现相同的效果,但非常很少有人这样做。

数据库连接和事务应该是短暂的。否则它们会积累锁并占用服务器上的资源,从而导致不同事务之间的阻塞甚至死锁。否则,即使有几个并发客户端,您也可能会遇到死锁或长时间延迟。这在 1990 年代是一个巨大的问题,当时还没有引入断开连接的操作和乐观并发。您尝试做的事情会让您回到 1990 年代。

差异确实是性能差 1000 倍,并且必须使用 10 倍以上的数据库服务器s 来处理相同数量的流量。

这就是为什么文档、课程和教程(好的)都显示在使用之前创建的连接和事务:

using(var cn=new SqlConnection(...))

    cn.Open();
    using(var tx=cn.BeginTransaction())
    
        using (var cmd1=new SqlCommand(sql1,cn,tx))
        
        ...
        
        using (var cmd2=new SqlCommand(sql2,cn,tx))
        
        ...
        
    

如果您使用显式数据库事务,您必须将活动事务传递给命令本身。这就是你得到的异常所说的。另一种方法是使用TransactionScope 并在其内部创建开放连接。在这种情况下,连接被隐式注册到事务中:

using(var cn=new SqlConnection(...))

    using(var scope=new TransactionScope())
    
        cn.Open();

        using (var cmd=new SqlCommand(sql,cn))
        
        ...
        
        ...
    

Dapper 是一个基于 ADO.NET 的瘦映射器,它不会取代它。这意味着您仍然必须正确使用 ADO.NET、连接和事务。如果要使用显式事务,需要通过transaction参数传递给QueryExecute

using(var cn=new SqlConnection(...))

    cn.Open();
    using(var tx=cn.BeginTransaction())
    
        var results1=cn.QueryAsync<City>(sql1,transaction:tx);
        
        var results2=cn.QueryAsync<City>(sql2,transaction:tx);
        
    

或者您可以使用TransactionScope

using(var scope=new TransactionScope())

    using(var cn=new SqlConnection(...))
            
        cn.Open();
        var results1=cn.QueryAsync<City>(sql1);
        
        var results2=cn.QueryAsync<City>(sql2);        
    

实现已经泄漏。 “存储库”(它实际上是 Data Access Object,而不是存储库)需要访问 _transaction 字段的值。或者您可以使用TransactionScope 而忘记UoW。毕竟,访问数据库是DAO/Repository 的工作,而不是 UoW 的工作。 也许您可以使用UoW 作为TransactionScope 上的瘦包装器,或者让Repository 创建并初始化UoW,并使用来自它所拥有的连接的显式事务。 p>

假设您使用TransactionScope,您的 UoW 应该只是一个包装器:

class UnitOfWork:IDisposable

    TransactionScope _scope=new TransactionScope();

    public void Dispose()
    
        _scope.Dispose();
    

“存储库”甚至不应该知道 UoW。它应该控制连接:

internal class CityRepository 

   string _connString;
   public CityRepository(IConfiguration configuration)
   
        _connString=configuration.GetConnectionString("DefaultConnection")
   
        
   public async Task<IEnumerable<City>> All()
   
       using(var cn=new SqlConnection(_connStr))
       
           var model = await Connection.QueryAsync<City>("SELECT * FROM DT_Inspection.City");
           return model.ToList();
       
   

只有控制器需要创建 UoW,然后,只有在有任何机会修改数据的情况下。读取不需要事务:​​

public class CityController : ControllerBase

    private ICityRepository _cityRepo;
    public CityController(ICityRepository cityRepo)
    
        _cityRepo=cityRepo;
    

    [HttpGet]
    public Task<IEnumerable<City>> GetAll()
    
       return _cityRepo.All();
    

    [HttpPost]
    public async Task Post(City[] cities)
    
        using(var uow=new UnitOfWork())
        
            foreach(var city in cities)
            
                _cityRepo.Insert(city);
            
        
    

【讨论】:

感谢您提供信息丰富的评论。这对我帮助很大。

以上是关于如何解决 Dapper - UnitOfWork 事务错误的主要内容,如果未能解决你的问题,请参考以下文章

如何使用带有代码优先实体框架的 UnitOfWork 模式防止重复条目?

UnitOfWork 是不是等于事务?还是不止于此?

Abp中的工作单元UnitOfWork的Aop是如何实现的

UnitOfWork + Repository 模式和实体框架模拟

dapper C#的Iconvertible问题

无法模拟unitOfWork和没有服务接口