TransactionScope オブジェクトを使用して、関数呼び出し間で渡す必要のない暗黙的なトランザクションをセットアップするのは素晴らしいことです。ただし、別の接続が既に開いているときに別の接続が開かれた場合、トランザクション コーディネーターは分散するトランザクションを静かにエスカレートします (MSDTC サービスを実行する必要があり、より多くのリソースと時間を消費します)。
したがって、これで問題ありません。
using (var ts = new TransactionScope())
{
using (var c = DatabaseManager.GetOpenConnection())
{
// Do Work
}
using (var c = DatabaseManager.GetOpenConnection())
{
// Do more work in same transaction using different connection
}
ts.Complete();
}
しかし、これはトランザクションをエスカレートします:
using (var ts = new TransactionScope())
{
using (var c = DatabaseManager.GetOpenConnection())
{
// Do Work
using (var nestedConnection = DatabaseManager.GetOpenConnection())
{
// Do more work in same transaction using different nested connection - escalated transaction to distributed
}
}
ts.Complete();
}
ネストされた接続を使用しながら、この方法でトランザクションのエスカレーションを回避するための推奨される方法はありますか?
現時点で思いつく最善の方法は、次のように、ThreadStatic 接続を確立し、Transaction.Current が設定されている場合にそれを再利用することです。
public static class DatabaseManager
{
private const string _connectionString = "data source=.\\sql2008; initial catalog=test; integrated security=true";
[ThreadStatic]
private static SqlConnection _transactionConnection;
[ThreadStatic] private static int _connectionNesting;
private static SqlConnection GetTransactionConnection()
{
if (_transactionConnection == null)
{
Transaction.Current.TransactionCompleted += ((s, e) =>
{
_connectionNesting = 0;
if (_transactionConnection != null)
{
_transactionConnection.Dispose();
_transactionConnection = null;
}
});
_transactionConnection = new SqlConnection(_connectionString);
_transactionConnection.Disposed += ((s, e) =>
{
if (Transaction.Current != null)
{
_connectionNesting--;
if (_connectionNesting > 0)
{
// Since connection is nested and same as parent, need to keep it open as parent is not expecting it to be closed!
_transactionConnection.ConnectionString = _connectionString;
_transactionConnection.Open();
}
else
{
// Can forget transaction connection and spin up a new one next time one's asked for inside this transaction
_transactionConnection = null;
}
}
});
}
return _transactionConnection;
}
public static SqlConnection GetOpenConnection()
{
SqlConnection connection;
if (Transaction.Current != null)
{
connection = GetTransactionConnection();
_connectionNesting++;
}
else
{
connection = new SqlConnection(_connectionString);
}
if (connection.State != ConnectionState.Open)
{
connection.Open();
}
return connection;
}
}
編集:したがって、上記のコードのように、トランザクションスコープ内にネストされているときに同じ接続を再利用することが答えである場合、トランザクション中にこの接続を破棄することの影響について疑問に思います。
私が見る限り(リフレクターを使用してコードを調べる)、接続の設定(接続文字列など)がリセットされ、接続が閉じられます。したがって (理論的には)、接続文字列を再設定し、後続の呼び出しで接続を開くと、接続が「再利用」され、エスカレーションが防止されます (私の最初のテストではこれに同意しています)。
それは少しハックに思えます...そして、オブジェクトが破棄された後はオブジェクトを使い続けるべきではないと述べているベストプラクティスがどこかにあるに違いありません!
ただし、シールされた SqlConnection をサブクラス化することはできず、トランザクションに依存しない接続プールに適したメソッドを維持したいので、より良い方法を見つけるのに苦労しています (しかし喜んでいます)。
また、アプリケーションコードがネストされた接続を開こうとした場合に例外をスローすることで、ネストされていない接続を強制できることに気付きました (ほとんどの場合、コードベースでは不要です)。
public static class DatabaseManager
{
private const string _connectionString = "data source=.\\sql2008; initial catalog=test; integrated security=true; enlist=true;Application Name='jimmy'";
[ThreadStatic]
private static bool _transactionHooked;
[ThreadStatic]
private static bool _openConnection;
public static SqlConnection GetOpenConnection()
{
var connection = new SqlConnection(_connectionString);
if (Transaction.Current != null)
{
if (_openConnection)
{
throw new ApplicationException("Nested connections in transaction not allowed");
}
_openConnection = true;
connection.Disposed += ((s, e) => _openConnection = false);
if (!_transactionHooked)
{
Transaction.Current.TransactionCompleted += ((s, e) =>
{
_openConnection = false;
_transactionHooked = false;
});
_transactionHooked = true;
}
}
connection.Open();
return connection;
}
}
ハックの少ないソリューションを引き続き評価します:)