web-dev-qa-db-ja.com

catchブロックでロールバックするためにSqlTransactionオブジェクトにアクセスできません

私は問題を抱えており、私が見つけたすべての記事や例はそれを気にしていないようです。

トランザクションでいくつかのデータベースアクションを実行したい。私がやりたいことは、ほとんどの例と非常に似ています:

_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()を実行していますが、どちらも複数の例外をスローする可能性があるため、少し危険です。

私は間違っていますか、またはほとんどの人はこのリスクを無視しますか?例外が発生した場合、ロールバックできる最善のソリューションは何ですか?

33
Marks
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;
    }
}
56
Dave Markle

型を入力して変数を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行を変更するだけで済みます。

8
Mike Trusov

これを使って

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

ところで-処理が成功した場合はコミットしませんでした

6
Itay Karo
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;
    }
}
3
Paul Talbot
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
Ibki

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を宣言するとき、接続オブジェクトはnot自動的に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')
_

接続文字列のアプリケーション名プロパティを介してセッションprogram_nameを設定することが可能であることに注意してください:_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.'
_

It seems .NET Core(2.1.0)は、現在ScopeがSqlConnectionの前または後に初期化されているかどうかにかかわらず、TransactionScopeアプローチをサポートしていません。

1
Hans

Microsoftサンプル、try/catchの外側にbegin transを配置します このmsdnリンクを参照 。 BeginTransactionメソッドは例外をスローする必要があると仮定しますORトランザクションを開始しますが、両方を実行することはできません(ただし、ドキュメントでは不可能とは言われていません))。

ただし、 TransactionScope を使用したほうがよい場合があります。これは、(それほどではないが)大量のリフティングを管理します。 this link

1
Daniel Renshaw