web-dev-qa-db-ja.com

SqlConnectionオブジェクトから保留中のトランザクションへの参照を取得できますか?

誰か(私以外)が次のコードを記述し、それをアセンブリにコンパイルするとします。

using (SqlConnection conn = new SqlConnection(connString)) 
{
    conn.Open();
    using (var transaction = conn.BeginTransaction())
    {
        /* Update something in the database */
        /* Then call any registered OnUpdate handlers */
        InvokeOnUpdate(conn);

        transaction.Commit();
    }
}

InvokeOnUpdate(IDbConnection conn)の呼び出しは、実装および登録できるイベントハンドラーを呼び出します。したがって、このハンドラーにはIDbConnectionオブジェクトへの参照がありますが、保留中のトランザクションへの参照はありません。取引を保留する方法はありますか? OnUpdateハンドラーで、次のようなものを実行したいと思います。

private void MyOnUpdateHandler(IDbConnection conn) 
{
    var cmd = conn.CreateCommand();
    cmd.CommandText = someSQLString;
    cmd.CommandType = CommandType.Text;

    cmd.ExecuteNonQuery();
}

ただし、cmd.ExecuteNonQuery()を呼び出すと、InvalidOperationExceptionがスローされます。

「ExecuteNonQueryでは、コマンドに割り当てられた接続が保留中のローカルトランザクションにある場合、コマンドにトランザクションが必要です。コマンドのTransactionプロパティが初期化されていません。」.

保留中のトランザクションでSqlCommandcmdを参加させることはできますか? IDbConnectionオブジェクトから保留中のトランザクションへの参照を取得できますか(必要に応じてリフレクションを使用できます)?

40
Rune

うわー、最初はこれを信じていませんでした。ローカルSQLServerトランザクションを使用する場合、CreateCommand()がコマンドにトランザクションを与えないこと、およびトランザクションがSqlConnectionオブジェクトに公開されていないことに驚いています。実際、SqlConnectionを反映すると、現在のトランザクションはそのオブジェクトに保存されません。以下の編集では、いくつかの内部クラスを介してオブジェクトを追跡するためのヒントをいくつか示しました。

メソッドを変更できないことは知っていますが、メソッドバーの周りでTransactionScopeを使用できますか?だからあなたが持っているなら:

public static void CallingFooBar()
{
   using (var ts=new TransactionScope())
   {
      var foo=new Foo();
      foo.Bar();
      ts.Complete();
   }
}

これは機能します。私はあなたと同様のコードを使用してテストしました。ラッパーを追加すると、もちろんこれを実行できればすべて正常に機能します。指摘したように、TransactionScope内で複数の接続が開かれている場合は、分散トランザクションにエスカレーションされます。分散トランザクション用にシステムが構成されていない限り、エラーが発生します。

DTCへの参加も、ローカルトランザクションよりも数倍遅くなります。

編集

本当にリフレクションを試して使用したい場合、SqlConnectionにはSqlInternalConnectionがあり、これにはSqlInternalTransactionを返すAvailableInternalTransactionのプロパティがあり、Parentのプロパティには必要なSqlTransactionが返されます。

10
JoshBerke

誰かがこれを達成するためのリフレクションコードに興味がある場合は、ここに行きます:

    private static readonly PropertyInfo ConnectionInfo = typeof(SqlConnection).GetProperty("InnerConnection", BindingFlags.NonPublic | BindingFlags.Instance);
    private static SqlTransaction GetTransaction(IDbConnection conn) {
        var internalConn = ConnectionInfo.GetValue(conn, null);
        var currentTransactionProperty = internalConn.GetType().GetProperty("CurrentTransaction", BindingFlags.NonPublic | BindingFlags.Instance);
        var currentTransaction = currentTransactionProperty.GetValue(internalConn, null);
        var realTransactionProperty = currentTransaction.GetType().GetProperty("Parent", BindingFlags.NonPublic | BindingFlags.Instance);
        var realTransaction = realTransactionProperty.GetValue(currentTransaction, null);
        return (SqlTransaction) realTransaction;
    }

ノート:

  • タイプは内部であり、プロパティはプライベートであるため、動的に使用することはできません
  • 内部型は、最初のConnectionInfoで行ったように、中間型を宣言することもできません。オブジェクトでGetTypeを使用する必要があります
18

DenisがVB.NETで作成したデコレータクラスのC#バージョンに興味がある人は、次のようになります。

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

namespace DataAccessLayer
{
    /// <summary>
    /// Decorator for the connection class, exposing additional info like it's transaction.
    /// </summary>
    public class ConnectionWithExtraInfo : IDbConnection
    {
        private IDbConnection connection = null;
        private IDbTransaction transaction = null;

        public IDbConnection Connection
        {
            get { return connection; }
        }

        public IDbTransaction Transaction
        {
            get { return transaction; }
        }

        public ConnectionWithExtraInfo(IDbConnection connection)
        {
            this.connection = connection;
        }

        #region IDbConnection Members

        public IDbTransaction BeginTransaction(IsolationLevel il)
        {
            transaction = connection.BeginTransaction(il);
            return transaction;
        }

        public IDbTransaction BeginTransaction()
        {
            transaction = connection.BeginTransaction();
            return transaction;
        }

        public void ChangeDatabase(string databaseName)
        {
            connection.ChangeDatabase(databaseName);
        }

        public void Close()
        {
            connection.Close();
        }

        public string ConnectionString
        {
            get 
            {
                return connection.ConnectionString; 
            }
            set 
            {
                connection.ConnectionString = value;
            }
        }

        public int ConnectionTimeout
        {
            get { return connection.ConnectionTimeout; }
        }

        public IDbCommand CreateCommand()
        {
            return connection.CreateCommand();
        }

        public string Database
        {
            get { return connection.Database; }
        }

        public void Open()
        {
            connection.Open();
        }

        public ConnectionState State
        {
            get { return connection.State; }
        }

        #endregion

        #region IDisposable Members

        public void Dispose()
        {
            connection.Dispose();
        }

        #endregion
    }
}
4
victorvartan

コマンドオブジェクトには、コンストラクタの1つを使用してのみトランザクションオブジェクトを割り当てることができます。 .NET 2.0アプローチを選択して、System.Transactions名前空間で定義されているTransactionScopeオブジェクトを使用できます(専用のアセンブリがあります)。

   using System.Transactions;

    class Foo
    {   
        void Bar()
        {
            using (TransactionScope scope = new TransactionScope())
            {
                // Data access
                // ...
                scope.Complete()
            }
        }
    }

System.Transactionsアプローチは、SQL Server 2005と組み合わせて軽量トランザクションコーディネーター(LTM)を使用します。トランザクションスコープで複数の接続オブジェクトを使用しないように注意してください。使用しないと、トランザクションは分散されていると見なされるため、昇格されます。このよりリソースを大量に消費するバージョンのトランザクションは、DTCによって処理されます。

3
lvaneenoo

私はシンプルの大きな支持者なので、トランザクションを公開するIDBConnection(DELEGATE PATTERN)のラッパーを作成するのはどうでしょうか。 (VB.NETコードについては申し訳ありませんが、私は今VB.NETでこれを書いています)このようなもの:

  Public class MyConnection
      Implements IDbConnection

      Private itsConnection as IDbConnection
      Private itsTransaction as IDbTransaction

      Public Sub New(ByVal conn as IDbConnection)
         itsConnection = conn
      End Sub

      //...  'All the implementations would look like
      Public Sub Dispose() Implements IDbConnection.Dispose
         itsConnection.Dispose()
      End Sub
      //...

      //     'Except BeginTransaction which would look like
       Public Overridable Function BeginTransaction() As IDbTransaction Implements IDbConnection.BeginTransaction
         itsTransaction = itsConnection.BeginTransaction()
         Return itsTransaction
       End Function  


      // 'Now you can create a property and use it everywhere without any hacks
       Public ReadOnly Property Transaction
          Get
              return itsTransaction
          End Get
       End Property

    End Class

したがって、これを次のようにインスタンス化します。

Dim myConn as new MyConnection(new SqlConnection(...))

その後、次を使用していつでもトランザクションを取得できます。

 myConn.Transaction
0
Denis