誰か(私以外)が次のコードを記述し、それをアセンブリにコンパイルするとします。
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オブジェクトから保留中のトランザクションへの参照を取得できますか(必要に応じてリフレクションを使用できます)?
うわー、最初はこれを信じていませんでした。ローカル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が返されます。
誰かがこれを達成するためのリフレクションコードに興味がある場合は、ここに行きます:
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;
}
ノート:
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
}
}
コマンドオブジェクトには、コンストラクタの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