Не удается получить доступ к объекту SqlTransaction для отката в блоке catch - PullRequest
33 голосов
/ 26 мая 2010

У меня проблема, и все статьи или примеры, которые я нашел, похоже, не заботятся об этом.

Я хочу выполнить некоторые действия с базой данных в транзакции. То, что я хочу сделать, очень похоже на большинство примеров:

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(). Большинство примеров просто делают Conn.Open() и Conn.BeginTransaction() перед блоком try, но я думаю, что это немного рискованно, поскольку оба могут выдавать несколько исключений.

Я ошибаюсь, или большинство людей просто игнорируют этот риск? Как лучше всего откатиться, если произойдет исключение?

Ответы [ 7 ]

56 голосов
/ 26 мая 2010
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;
    }
}
8 голосов
/ 10 июля 2013

Мне не нравится вводить типы и устанавливать переменные в 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 строку.

6 голосов
/ 26 мая 2010

используйте это

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

Кстати - Вы не зафиксировали его в случае успешной обработки

3 голосов
/ 26 мая 2010
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;
    }
}
1 голос
/ 30 ноября 2018

Когда я нашел этот вопрос в первый раз в конце 2018 года, я не думал, что может быть ошибка в тогдашнем ответе с наибольшим количеством голосов, но это так. Сначала я подумал о том, чтобы просто прокомментировать ответ, но потом снова захотел подкрепить свою заявку своими ссылками. И тесты, которые я сделал (на основе .Net Framework 4.6.1 и .Net Core 2.1.)

Учитывая ограничение OP, транзакция должна быть объявлена ​​в соединении, что оставляет нас к двум различным реализациям, уже упомянутым в других ответах:

Использование 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;
    }
}

Вы должны знать, что при объявлении TransactionScope в SqlConnection этот объект соединения не автоматически зачисляется в транзакцию, вместо этого вы должны явным образом подключить его к 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, соединение не знает о области и не косвенно подключается к так называемой внешней транзакции .

Более глубокий анализ для ботаников базы данных

Чтобы копнуть еще глубже, если выполнение приостанавливается после 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.'

Кажется, .NET Core (2.1.0) в настоящее время не поддерживает подход TransactionScope, независимо от того, инициализируется ли Scope до или после SqlConnection.

1 голос
/ 15 сентября 2014
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();
   }
}
1 голос
/ 26 мая 2010

Примеры Microsoft, поместите начало транс за пределами try / catch см. Эту ссылку MSDN . Я предполагаю, что метод BeginTransaction должен либо генерировать исключение, либо начинать транзакцию, но не оба одновременно (хотя в документации не говорится, что это невозможно).

Однако вам может быть лучше использовать TransactionScope , который управляет многими (не очень) тяжелыми действиями для вас: эта ссылка

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...