Cannot access SqlTransaction object to rollback in catch block

前端 未结 7 1052
我寻月下人不归
我寻月下人不归 2020-12-02 15:43

I\'ve got a problem, and all articles or examples I found seem to not care about it.

I want to do some database actions in a transaction. What I want to do is very s

相关标签:
7条回答
  • 2020-12-02 16:00
    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;
        }
    }
    
    0 讨论(0)
  • 2020-12-02 16:11

    When I found this question the first time end of 2018 I didn't thought there could be a bug in the then top voted answer, but there it goes. I first thought about simply commenting the answer but then again I wanted to back up my claim with my own references. And tests I did (based on .Net Framework 4.6.1 and .Net Core 2.1.)

    Given the OP's constraint, the transaction should be declared within the connection which leaves us to the 2 different implementations already mentioned in other answers:

    Using 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;
        }
    }
    

    Using 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;
        }
    }
    

    You should be aware that when declaring a TransactionScope within a SqlConnection that connection object is not automatically enlisted into the Transaction, instead you have to enlist it explicitely with conn.EnlistTransaction(Transaction.Current);

    Test and prove
    I have prepared a simple table in a SQL Server database:

    SELECT * FROM [staging].[TestTable]
    
    Column1
    -----------
    1
    

    The update query in .NET is as follows:

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

    And right after command.ExecuteNonQuery() an exception is thrown:

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

    Here is the full example for your reference:

    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;
        }
    }
    

    If the test is executed it throws an exception before the TransactionScope is completed and the update is not applied to the table (transactional rollback) and the value rests unchanged. This is the intended behavior as everybody would expect.

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

    What happens now if we forgot to enlist the connection in the transaction with conn.EnlistTransaction(Transaction.Current);?

    Rerunning the example provokes the exception again and the execution flow jumps immediately to the catch block. Albeit ts.Complete(); is never called the table value has changed:

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

    As the transaction scope is declared after the SqlConnection the connection is not aware of the scope and does not implicitly enlist in the so called ambient transaction.

    Deeper analysis for database nerds

    To dig even deeper, if execution pauses after command.ExecuteNonQuery(); and before the exception is thrown we are able to query the transaction on the database (SQL Server) as follows:

    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')
    

    Do note that it is possible to set the session program_name through the Application Name property in the connection string: Application Name=TransactionScopeTest;

    The currently existing transaction is unfolding below:

    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
    

    Without the conn.EnlistTransaction(Transaction.Current); no transaction is bound to the active connection and therefore the changes do not happen under a transactional context:

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

    Remarks .NET Framework vs. .NET Core
    During my tests with .NET Core I came across the following exception:

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

    It seems .NET Core (2.1.0) does currently not support the TransactionScope approach no matter whether the Scope is initialized before or after the SqlConnection.

    0 讨论(0)
  • 2020-12-02 16:14

    I don't like typing types and setting variables to null, so:

    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 */
    }
    

    And if you wanted to switch to MySql or another provider, you'd only have to modify 1 line.

    0 讨论(0)
  • 2020-12-02 16:15

    use this

    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;
        }
    }
    

    BTW - You did not commit it in case of successful processing

    0 讨论(0)
  • 2020-12-02 16:15

    Microsoft samples, place the begin trans outside of the try/catch see this msdn link. I assume that the BeginTransaction method should either throw an exception OR begin a transaction but never both (although the documentation does not say this is impossible).

    However, you may be better of using TransactionScope which manages a lot of the (not so) heavy lifting for you: this link

    0 讨论(0)
  • 2020-12-02 16:21
    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;
        }
    }
    

    or you could go even cleaner and easier and use this:

    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;
        }
    }
    
    0 讨论(0)
提交回复
热议问题