Является ли лучшей практикой явное обращение к транзакциям или исключение исключает неявный откат?

В приведенном ниже коде, если какое-либо исключение возникает при выполнении операторов SQL, мы должны ожидать неявного откат транзакции по мере того, как транзакция не была выполнена, она выходит за пределы области действия и удаляется:

using (DbTransaction tran = conn.BeginTransaction())
{
    //
    // Execute SQL statements here...
    //
    tran.Commit();
}

Является ли это вышеприведенной практикой или нужно уловить исключение и явно вызвать вызов tran.Rollback(), как показано ниже:

using (DbTransaction tran = conn.BeginTransaction())
{
    try
    {
        //
        // Execute SQL statements here...
        //
        tran.Commit();
    }
    catch
    {
        tran.Rollback();
        throw;
    }
}

Ответ 1

Бывший. Если вы просматриваете образцы MSND по подобным темам, например TransactionScope, все они предпочитают неявный откат. Для этого есть разные причины, но я просто дам вам очень простой: к тому времени, когда вы поймаете исключение, транзакция, возможно, уже откат. Многие ошибки откатываются от ожидающей транзакции, а затем возвращают управление клиенту, где ADO.Net вызывает исключение CLR SqlException после того, как транзакция уже откатилась на сервере (1205 DEADLOCK - типичный пример такой ошибки), поэтому явное Rollback() вызов - это, в лучшем случае, не-op, а хуже ошибка. Поставщик DbTransaction (например, SqlTransaction) должен знать, как обрабатывать этот случай, например. потому что есть явный чат между сервером и клиентом, уведомляющий о том, что транзакция откатилась уже, и метод Dispose() делает все правильно.

Вторая причина заключается в том, что транзакции могут быть вложенными, но семантика ROLLBACK заключается в том, что один откат откатывает все транзакции, поэтому вам нужно только один раз вызвать его (в отличие от Commit(), который совершает только внутреннюю транзакцию и должен называть их парными для каждого начала). Опять же, Dispose() делает правильные вещи.

Update

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

Ответ 2

Вы можете пойти в любом случае, первый из них будет более кратким, причем последний будет более наглядным.

Оговорка с первым подходом заключалась бы в том, что вызов RollBack при удалении транзакции зависит от конкретной реализации драйвера. Надеюсь, что почти все соединители .NET это делают. SqlTransaction делает:

private void Dispose(bool disposing)
{
    Bid.PoolerTrace("<sc.SqlInteralTransaction.Dispose|RES|CPOOL> %d#, Disposing\n", this.ObjectID);
    if (disposing && (this._innerConnection != null))
    {
        this._disposing = true;
        this.Rollback();
    }
}

MySQL:

protected override void Dispose(bool disposing)
{
  if ((conn != null && conn.State == ConnectionState.Open || conn.SoftClosed) && open)
    Rollback();
  base.Dispose(disposing);
}

Предостережение со вторым подходом небезопасно вызывать RollBack без другого try-catch. Это явно указано в документации.

Короче говоря, что лучше: это зависит от водителя, но обычно лучше идти первым, по причинам, упомянутым Ремусом.

Кроме того, см. Что происходит с незафиксированной транзакцией при закрытии соединения? относительно того, как обрабатывать удаление соединений и откаты.

Ответ 3

Я склонен соглашаться с отказом "Неявный" на основе путей исключения. Но, очевидно, это зависит от того, где вы находитесь в стеке и что вы пытаетесь сделать (т.е. Класс DBTranscation ловит исключение и выполняет очистку, или он пассивно не "совершает"?).

Здесь случай, когда неявное обращение имеет смысл (возможно):

static T WithTranaction<T>(this SqlConnection con, Func<T> do) {
    using (var txn = con.BeginTransaction()) {
        return do();
    }
}

Но, если API отличается, обработка commit также может быть (предоставлена, это:

static T WithTranaction<T>(this SqlConnection con, Func<T> do, 
    Action<SqlTransaction> success = null, Action<SqlTransaction> failure = null) 
{
    using (var txn = con.BeginTransaction()) {
        try {
            T t = do();
            success(txn); // does it matter if the callback commits?
            return t;
        } catch (Exception e) {
            failure(txn); // does it matter if the callback rolls-back or commits?
            // throw new Exception("Doh!", e); // kills the transaction for certain
            // return default(T); // if not throwing, we need to do something (bogus)
        }
    }
}

Я не могу думать о слишком многих случаях, когда явное откатывание - это правильный подход, за исключением тех случаев, когда существует строгая политика управления изменениями, которая должна быть соблюдена. Но опять же, я немного замешан на восприятии.