无法访问 SqlTransaction 对象以在 catch 块中回滚

Posted

技术标签:

【中文标题】无法访问 SqlTransaction 对象以在 catch 块中回滚【英文标题】:Cannot access SqlTransaction object to rollback in catch block 【发布时间】:2011-02-24 02:56:19 【问题描述】:

我遇到了一个问题,我找到的所有文章或示例似乎都不关心它。

我想在事务中执行一些数据库操作。我想做的与大多​​数示例非常相似:

using (SqlConnection Conn = new SqlConnection(_ConnectionString))

    try
    
        Conn.Open();
        SqlTransaction Trans = Conn.BeginTransaction();

        using (SqlCommand Com = new SqlCommand(ComText, Conn))
        
            /* DB work */
        
    
    catch (Exception Ex)
    
        Trans.Rollback();
        return -1;
    

但问题是SqlTransaction Trans 是在try 块内声明的。所以它在catch() 块中是不可访问的。大多数示例只是在try 块之前执行Conn.Open()Conn.BeginTransaction(),但我认为这有点冒险,因为两者都可能引发多个异常。

我错了,还是大多数人都忽略了这种风险? 如果发生异常,能够回滚的最佳解决方案是什么?

【问题讨论】:

P.S.您确定要返回 -1(错误代码)而不是抛出异常吗? 【参考方案1】:
using (var Conn = new SqlConnection(_ConnectionString))

    SqlTransaction trans = null;
    try
    
        Conn.Open();
        trans = Conn.BeginTransaction();

        using (SqlCommand Com = new SqlCommand(ComText, Conn, trans))
        
            /* DB work */
        
        trans.Commit();
    
    catch (Exception Ex)
    
        if (trans != null) trans.Rollback();
        return -1;
    

或者你可以更简洁、更轻松地使用它:

using (var Conn = new SqlConnection(_ConnectionString))

    try
    
        Conn.Open();
        using (var ts = new System.Transactions.TransactionScope())
        
            using (SqlCommand Com = new SqlCommand(ComText, Conn))
            
                /* DB work */
            
            ts.Complete();
        
    
    catch (Exception Ex)
         
        return -1;
    

【讨论】:

第二个版本真的是在抛出异常的时候回滚吗?编辑:好的,在阅读文档后我已经看到了。 在您的第一个示例中,您不需要指定sqlcommand与事务相关联吗?比如using (SqlCommand Com = new SqlCommand(ComText, Conn, **trans**))?或者那是不必要的?它是隐式关联的吗? 好的,谢谢。使用 TransactionScope,您不需要,但我在第一个示例中省略了它。相应地进行了编辑。 在第二个示例中,我认为您需要使用块关闭 TransactionScope 内的连接,否则当它离开块时会出现异常,说明连接未关闭。 只需将此行 trans = Conn.BeginTransaction(); 包装在 using 语句中并阻塞,然后如果在调用提交之前发生异常,则将在处理之前为您调用 Rollback()【参考方案2】:

我不喜欢键入类型并将变量设置为 null,所以:

try

    using (var conn = new SqlConnection(/* connection string or whatever */))
    
        conn.Open();

        using (var trans = conn.BeginTransaction())
        
            try
            
                using (var cmd = conn.CreateCommand())
                
                    cmd.Transaction = trans;
                    /* setup command type, text */
                    /* execute command */
                

                trans.Commit();
            
            catch (Exception ex)
            
                trans.Rollback();
                /* log exception and the fact that rollback succeeded */
            
        
    

catch (Exception ex)

    /* log or whatever */

如果你想切换到 mysql 或其他提供程序,你只需要修改 1 行。

【讨论】:

【参考方案3】:

使用这个

using (SqlConnection Conn = new SqlConnection(_ConnectionString))

    SqlTransaction Trans = null;
    try
    
        Conn.Open();
        Trans = Conn.BeginTransaction();

        using (SqlCommand Com = new SqlCommand(ComText, Conn))
        
            /* DB work */
        
    
    catch (Exception Ex)
    
        if (Trans != null)
            Trans.Rollback();
        return -1;
    

顺便说一句 - 如果处理成功,您没有提交它

【讨论】:

【参考方案4】:
using (SqlConnection Conn = new SqlConnection(_ConnectionString))

    try
    
        Conn.Open();
        SqlTransaction Trans = Conn.BeginTransaction();

        try 
        
            using (SqlCommand Com = new SqlCommand(ComText, Conn))
            
                /* DB work */
            
        
        catch (Exception TransEx)
        
            Trans.Rollback();
            return -1;
        
    
    catch (Exception Ex)
    
        return -1;
    

【讨论】:

虽然还有更多的代码需要编写,但这提供了最好的粒度来确定每个步骤为什么会失败。但是,请注意 SqlCommand 必须与事务相关联。【参考方案5】:

当我在 2018 年底第一次发现这个问题时,我没想到当时投票最多的答案中可能存在错误,但事实就是如此。我首先考虑简单地评论答案,但后来我想用我自己的参考来支持我的主张。以及我所做的测试(基于 .Net Framework 4.6.1 和 .Net Core 2.1。)

鉴于 OP 的约束,事务应在连接中声明,这使我们只能使用其他答案中已经提到的 2 个不同的实现:

使用 TransactionScope

using (SqlConnection conn = new SqlConnection(conn2))

    try
    
        conn.Open();
        using (TransactionScope ts = new TransactionScope())
        
            conn.EnlistTransaction(Transaction.Current);
            using (SqlCommand command = new SqlCommand(query, conn))
            
                command.ExecuteNonQuery();
                //TESTING: throw new System.InvalidOperationException("Something bad happened.");
            
            ts.Complete();
        
    
    catch (Exception)
    
        throw;
    

使用 SqlTransaction

using (SqlConnection conn = new SqlConnection(conn3))

    try
    
        conn.Open();
        using (SqlTransaction ts = conn.BeginTransaction())
        
            using (SqlCommand command = new SqlCommand(query, conn, ts))
            
                command.ExecuteNonQuery();
                //TESTING: throw new System.InvalidOperationException("Something bad happened.");
            
            ts.Commit();
        
    
    catch (Exception)
    
        throw;
    

您应该知道,当在 SqlConnection 中声明一个 TransactionScope 时,该连接对象不会自动加入到 Transaction 中,而您必须使用 conn.EnlistTransaction(Transaction.Current); 明确地加入它

测试和证明 我在 SQL Server 数据库中准备了一个简单的表:

SELECT * FROM [staging].[TestTable]

Column1
-----------
1

.NET中的更新查询如下:

string query = @"UPDATE staging.TestTable
                    SET Column1 = 2";

在 command.ExecuteNonQuery() 之后立即抛出异常:

command.ExecuteNonQuery();
throw new System.InvalidOperationException("Something bad happened.");

这是供您参考的完整示例:

string query = @"UPDATE staging.TestTable
                    SET Column1 = 2";

using (SqlConnection conn = new SqlConnection(conn2))

    try
    
        conn.Open();
        using (TransactionScope ts = new TransactionScope())
        
            conn.EnlistTransaction(Transaction.Current);
            using (SqlCommand command = new SqlCommand(query, conn))
            
                command.ExecuteNonQuery();
                throw new System.InvalidOperationException("Something bad happened.");
            
            ts.Complete();
        
    
    catch (Exception)
    
        throw;
    

如果执行测试,它会在 TransactionScope 完成之前引发异常,并且更新不会应用于表(事务回滚)并且值保持不变。这是每个人都期望的预期行为。

Column1
-----------
1

如果我们忘记在与conn.EnlistTransaction(Transaction.Current); 的事务中登记连接,现在会发生什么?

重新运行示例再次引发异常,执行流程立即跳转到 catch 块。尽管从未调用过ts.Complete();,但表值已更改:

Column1
-----------
2

由于事务范围是在 SqlConnection 之后声明的,因此连接不知道范围,也不会隐式加入所谓的ambient transaction。

对数据库书呆子的深入分析

为了更深入地挖掘,如果在command.ExecuteNonQuery(); 之后和抛出异常之前暂停执行,我们可以查询数据库(SQL Server)上的事务,如下所示:

SELECT tst.session_id, tat.transaction_id, is_local, open_transaction_count, transaction_begin_time, dtc_state, dtc_status
  FROM sys.dm_tran_session_transactions tst
  LEFT JOIN sys.dm_tran_active_transactions tat
  ON tst.transaction_id = tat.transaction_id
  WHERE tst.session_id IN (SELECT session_id FROM sys.dm_exec_sessions WHERE program_name = 'TransactionScopeTest')

请注意,可以通过连接字符串中的应用程序名称属性设置会话程序名称: Application Name=TransactionScopeTest;

当前存在的交易正在以下展开:

session_id  transaction_id       is_local open_transaction_count transaction_begin_time  dtc_state   dtc_status
----------- -------------------- -------- ---------------------- ----------------------- ----------- -----------
113         6321722              1        1                      2018-11-30 09:09:06.013 0           0

没有conn.EnlistTransaction(Transaction.Current);,没有事务绑定到活动连接,因此更改不会在事务上下文下发生:

session_id  transaction_id       is_local open_transaction_count transaction_begin_time  dtc_state   dtc_status
----------- -------------------- -------- ---------------------- ----------------------- ----------- -----------

备注 .NET Framework 与 .NET Core 在使用 .NET Core 进行测试期间,我遇到了以下异常:

System.NotSupportedException: 'Enlisting in Ambient transactions is not supported.'

seems.NET Core (2.1.0) 目前不支持 TransactionScope 方式,无论 Scope 是在 SqlConnection 之前还是之后初始化。

【讨论】:

【参考方案6】:

Microsoft 示例,将 begin trans 放在 try/catch see this msdn link 之外。我假设 BeginTransaction 方法应该要么抛出异常,要么开始事务,但绝不应该两者都发生(尽管文档没有说这是不可能的)。

但是,您最好使用TransactionScope,它可以为您管理很多(不是那么)繁重的工作:this link

【讨论】:

【参考方案7】:
SqlConnection conn = null;
SqlTransaction trans = null;

try

   conn = new SqlConnection(_ConnectionString);
   conn.Open();
   trans = conn.BeginTransaction();
   /*
    * DB WORK
    */
   trans.Commit();

catch (Exception ex)

   if (trans != null)
   
      trans.Rollback();
   
   return -1;

finally

   if (conn != null)
   
      conn.Close();
   

【讨论】:

以上是关于无法访问 SqlTransaction 对象以在 catch 块中回滚的主要内容,如果未能解决你的问题,请参考以下文章

CommandBuilder 和 SqlTransaction 插入/更新行

为啥使用带有 SqlTransaction 的 using 语句?

将 SqlTransaction 与 SqlDataReader 一起使用

访问 NSMutableArray 对象以在 TableView 上使用

SqlTransaction

无法访问特定目录以在 ec2 上运行站点