SqlConnectionオブジェクトから保留中のトランザクションへの参照を取得できますか?
-
03-07-2019 - |
質問
誰か(私以外)が次のコードを書いて、それをアセンブリにコンパイルするとします:
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にはコマンドが必要です トランザクションがあるとき コマンドに割り当てられた接続は 保留中のローカルトランザクション。の コマンドのトランザクションプロパティ 初期化されていません"。
何らかの方法で、SqlCommand cmdを保留中のトランザクションに参加させることはできますか? IDbConnectionオブジェクトから保留中のトランザクションへの参照を取得できます(必要に応じてリフレクションを使用できます)?
解決
最初は信じられませんでした。ローカルのSQL Serverトランザクションを使用している場合、 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のプロパティがあり、これには必要なSqlTransactionを返すParentのプロパティがあります。
他のヒント
これを実現するためにリフレクションコードに興味がある人は、ここに行きます:
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を使用する必要があります
デニスが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
}
}
コマンドオブジェクトには、そのコンストラクタの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によって処理されます。
私はシンプルの大提唱者なので、トランザクションを公開する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
.Net 4.5で誰かがこの問題に直面した場合、 Transaction.Current
を使用できます
System.Transactions
で。