techbank.jp コミュニティ

     あれ?どうやるんだっけ?を解決する

坊やがゆくtech

TransactionScope中に例外が発生したのにロールバックされない

using System;
using System.Collections.Generic;
using System.Text;

using System.Data;
using System.Data.SQLite;
using System.Transactions;

namespace ConsoleApplication1
{
  class Program
  {
      static void Main(string[] args)
      {
          using (SQLiteConnection cnn = new SQLiteConnection("Data Source=:memory:")) // SQLiteはデフォルトで自動コミットモード
          {
              cnn.Open(); // この時点でトランザクションスコープに含まれる必要がある

              using (TransactionScope tran = new TransactionScope())
              {
                  try
                  {
                      using (SQLiteCommand cmd = cnn.CreateCommand())
                      {
                          cmd.CommandText = "CREATE TABLE EMPLOYEES (ID INTEGER PRIMARY KEY, NAME NVARCHAR(256))";
                          cmd.ExecuteNonQuery();

                          cmd.CommandText = "INSERT INTO EMPLOYEES (NAME) VALUES('HOGE')";
                          cmd.ExecuteNonQuery();

                          throw new Exception("ロールバックしたい!");
                      }
                      tran.Complete(); // 到達しない
                  }
                  catch { }
              }
              using (SQLiteCommand cmd = cnn.CreateCommand())
              {
                  cmd.CommandText = "SELECT COUNT(*) FROM EMPLOYEES";
                  SQLiteDataAdapter adapter = new SQLiteDataAdapter(cmd);
                  DataSet ds = new DataSet();
                  adapter.Fill(ds);
                  Console.WriteLine(ds.Tables[0].Rows[0][0]); // 1 (ロールバックされていない)
              }
          }
          Console.ReadKey();
      }
  }
}

上記のコードでは例外でTransactionScopeを抜けたにも関わらず更新がロールバックされません。

TransactionScopeのusingは一見トランザクションを開始しているように見えますが、このコードを実際にプロファイルで確認するとトランザクションは開始されていません。

実際にトランザクションの対象となるのはConnectionであり、TransactionScope内のConnection.Open()がトランザクションに参加するようです。
ですのでサンプルのコードはConnection.Open()がTransactionScopeの外側にあるためトランザクションに参加しておらず、自動コミットモードで更新が行われているためロールバックされないのです。

なのでこれはNGで

using (SQLiteConnection cnn = new SQLiteConnection("Data Source=:memory:"))
{
  cnn.Open(); // NG
  using (TransactionScope tran = new TransactionScope())
  {
      tran.Complete();
  }
}

これはOK

using (TransactionScope tran = new TransactionScope())
using (SQLiteConnection cnn = new SQLiteConnection("Data Source=:memory:"))
{
  cnn.Open(); // OK
  tran.Complete();
}

教訓:ConnectionはTransactionScopeの内側に書く(定型文!)

コメント

 

けろ-みお 発言:

やはり、TransactionScopeは、接続単位で紐づけているんですね。恐らくTransactionScopeは、紐づけられた接続に対し、BeginTransaction を裏で発行しているんじゃないかと推測します。

このコードはバグになりやすいコードなので、気を付けた方がいいっていうのが人目でわかる良いサンプルコードですw

11月 16, 2009 8:10 午前
 

PoohKid 発言:

けろ-みおさん

そうですね、更新とは全く関係のない参照のみのコネクションを重ねただけで分散トランザクションの対象になってしまうくらいですから、スコープ内のConnection.Openメソッドを裏で拾ってる感じですよね。

11月 17, 2009 12:30 午前

コメントを残す

(必須)  
(オプション)
(必須)  
techbank.jp