SQL Server と System.Transactions の統合
適用対象: .NET Framework .NET .NET Standard
.NET には、System.Transactions 名前空間を介してアクセスできるトランザクション フレームワークが含まれています。 このフレームワークにより、ADO.NET を含む .NET に完全に統合された方法でトランザクションが公開されます。
プログラミング上の強化に加えて、System.Transactions と ADO.NET の連係により、トランザクション処理が最適化されます。 昇格可能なトランザクションとは、必要に応じて完全な分散トランザクションに自動的に昇格する、軽量の (ローカル) トランザクションです。
SQL Server を使用すると、Microsoft SqlClient Data Provider for SQL Server で昇格可能なトランザクションがサポートされます。 昇格可能なトランザクションは、必要な場合以外、分散トランザクションのオーバーヘッドの増加を引き起こすことはありません。 昇格可能なトランザクションは自動的に処理され、開発者による介入は必要ありません。
昇格可能なトランザクションの作成
Microsoft SqlClient Data Provider for SQL Server では昇格可能なトランザクションをサポートしており、System.Transactions 名前空間内のクラスを介して処理されます。 昇格可能なトランザクションでは、必要が生じるまで分散トランザクションの作成を延期することで、分散トランザクションが最適化されます。 必要なリソース マネージャーが 1 つだけである場合は、分散トランザクションは発生しません。
Note
部分信頼のシナリオで分散トランザクションに昇格するには、 DistributedTransactionPermission が必要です。
昇格可能なトランザクションのシナリオ
分散トランザクションは一般的にシステム リソースを大量に消費するため、トランザクションでアクセスされるすべてのリソース マネージャーを統合する、Microsoft Distributed Transaction Coordinator (MS DTC) で管理されます。 昇格可能なトランザクションは特殊な形式の System.Transactions トランザクションで、単純な SQL Server トランザクションに処理を効果的に委任できます。 System.Transactions、 Microsoft.Data.SqlClient、および SQL Server では、トランザクションの処理に関連する作業が調整され、必要に応じて、トランザクションは完全な分散トランザクションに昇格されます。
昇格可能なトランザクションを使用する利点は、アクティブな TransactionScope トランザクションによって接続が開かれ、その他の接続が開いていない場合に、完全な分散トランザクションによるオーバーヘッドが生じることなく、トランザクションが軽量なトランザクションとしてコミットされることです。
接続文字列キーワード
ConnectionString プロパティではキーワード Enlist
がサポートされており、 Microsoft.Data.SqlClient によってトランザクションのコンテキストが検出され、分散トランザクションに接続が自動的に参加するかどうかが示されます。 Enlist=true
である場合は、開いているスレッドの現在のトランザクション コンテキストに接続が自動的に参加します。 Enlist=false
である場合、 SqlClient
接続は分散トランザクションとは対話しません。 Enlist
の既定値は true です。 Enlist
が接続文字列で指定されていない場合、接続が開いたときに分散トランザクションがあればそれに自動的に参加します。
参加する Transaction Binding
トランザクションと接続の関連付けは、 SqlConnection 接続文字列の System.Transactions
キーワードによって制御されます。 TransactionBinding の SqlConnectionStringBuilderプロパティを介して利用することもできます。
次の表で使用可能な値について説明します。
キーワード | 説明 |
---|---|
Implicit Unbind | これが既定値です。 完了時に接続がトランザクションからデタッチされ、自動コミット モードに切り替わります。 |
Explicit Unbind | トランザクションが閉じられるまで、接続がトランザクションにアタッチされたままになります。 関連付けられているトランザクションがアクティブでない場合や、 Currentと一致しない場合、接続は失敗します。 |
TransactionScope の使用
TransactionScope クラスでは、接続を分散トランザクションに暗黙的に参加させることで、コード ブロックがトランザクションのコード ブロックになります。 Complete ブロックの末尾では、終了する前に TransactionScope メソッドを呼び出す必要があります。 ブロックを終了すると、 Dispose メソッドが呼び出されます。 例外がスローされてコードがスコープから外れている場合は、トランザクションがアボートされたと見なされます。
使用中のブロックを終了したときに using
オブジェクトで Dispose が呼び出されるように、 TransactionScope ブロックを使用することをお勧めします。 保留中のトランザクションのコミットまたはロールバックに失敗すると、 TransactionScope の既定のタイムアウトが 1 分であるため、パフォーマンスが大幅に低下する場合があります。 using
ステートメントを使用しない場合は、すべての処理を Try
ブロックで実行し、 Dispose メソッドを Finally
ブロック内で明示的に呼び出す必要があります。
TransactionScope内で例外が発生した場合、そのトランザクションは矛盾しているとしてマークされ、破棄されます。 トランザクションは、 TransactionScope が破棄されるとロールバックされます。 例外が発生しない場合は、参加しているトランザクションがコミットされます。
Note
既定では、 TransactionScope
クラスは、 IsolationLevel が Serializable
であるトランザクションを作成します。 使用しているアプリケーションによっては、アプリケーション内での競合を回避するために、分離レベルを低下させる必要がある場合があります。
Note
データベース リソースを大量に消費するため、分散トランザクション内で実行するのは更新、挿入、削除だけにすることをお勧めします。 Select ステートメントを使用すると、データベース リソースが不必要にロックされることがあり、シナリオによっては選択にトランザクションを使用する必要がある場合があります。 処理済みのその他のリソース マネージャーに関係する場合以外、データベース以外の処理はトランザクションのスコープ外で実行する必要があります。 トランザクションのスコープ内で例外が発生するとトランザクションのコミットが防止されますが、 TransactionScope クラスでは、作成したコードによってトランザクションのスコープ外で行われた変更はロールバックされません。 トランザクションがロールバックされたときに何らかの動作を行うようにする場合は、 IEnlistmentNotification インターフェイスを独自に実装し、トランザクションに明示的に参加する必要があります。
例
System.Transactions を使用する場合は、System.Transactions.dll への参照が必要になります。
次の関数は、 SqlConnection ブロックでラップされている、異なる 2 つの TransactionScope オブジェクトで表された異なる 2 つの SQL Server インスタンスに対する、昇格可能なトランザクションを作成する方法を示しています。
次のコードは、TransactionScope ステートメントを使用して using
ブロックを作成し、最初の接続を開きます。これにより、トランザクションが TransactionScope に自動参加します。
このトランザクションは、完全な分散トランザクションとしてではなく、最初に軽量のトランザクションとして参加します。 2 つ目の接続は、1 つ目の接続のコマンドで例外がスローされなかった場合にのみ、 TransactionScope に参加します。 2 つ目の接続が開かれると、トランザクションが完全な分散トランザクションに自動的に昇格されます。
その後、Complete メソッドが呼び出され、例外がスローされていない場合にのみ、トランザクションがコミットされます。 TransactionScope ブロックの任意の場所で例外がスローされると、 Complete
が呼び出されず、 TransactionScope ブロックの最後で using
が破棄されたときに分散トランザクションがロールバックされます。
using System;
using System.Transactions;
using Microsoft.Data.SqlClient;
class Program
{
static void Main(string[] args)
{
string connectionString = "Data Source = localhost; Integrated Security = true; Initial Catalog = AdventureWorks";
string commandText1 = "INSERT INTO Production.ScrapReason(Name) VALUES('Wrong size')";
string commandText2 = "INSERT INTO Production.ScrapReason(Name) VALUES('Wrong color')";
int result = CreateTransactionScope(connectionString, connectionString, commandText1, commandText2);
Console.WriteLine("result = " + result);
}
static public int CreateTransactionScope(string connectString1, string connectString2,
string commandText1, string commandText2)
{
// Initialize the return value to zero and create a StringWriter to display results.
int returnValue = 0;
System.IO.StringWriter writer = new System.IO.StringWriter();
// Create the TransactionScope in which to execute the commands, guaranteeing
// that both commands will commit or roll back as a single unit of work.
using (TransactionScope scope = new TransactionScope())
{
using (SqlConnection connection1 = new SqlConnection(connectString1))
{
try
{
// Opening the connection automatically enlists it in the
// TransactionScope as a lightweight transaction.
connection1.Open();
// Create the SqlCommand object and execute the first command.
SqlCommand command1 = new SqlCommand(commandText1, connection1);
returnValue = command1.ExecuteNonQuery();
writer.WriteLine("Rows to be affected by command1: {0}", returnValue);
// if you get here, this means that command1 succeeded. By nesting
// the using block for connection2 inside that of connection1, you
// conserve server and network resources by opening connection2
// only when there is a chance that the transaction can commit.
using (SqlConnection connection2 = new SqlConnection(connectString2))
try
{
// The transaction is promoted to a full distributed
// transaction when connection2 is opened.
connection2.Open();
// Execute the second command in the second database.
returnValue = 0;
SqlCommand command2 = new SqlCommand(commandText2, connection2);
returnValue = command2.ExecuteNonQuery();
writer.WriteLine("Rows to be affected by command2: {0}", returnValue);
}
catch (Exception ex)
{
// Display information that command2 failed.
writer.WriteLine("returnValue for command2: {0}", returnValue);
writer.WriteLine("Exception Message2: {0}", ex.Message);
}
}
catch (Exception ex)
{
// Display information that command1 failed.
writer.WriteLine("returnValue for command1: {0}", returnValue);
writer.WriteLine("Exception Message1: {0}", ex.Message);
}
}
// If an exception has been thrown, Complete will not
// be called and the transaction is rolled back.
scope.Complete();
}
// The returnValue is greater than 0 if the transaction committed.
if (returnValue > 0)
{
writer.WriteLine("Transaction was committed.");
}
else
{
// You could write additional business logic here, notify the caller by
// throwing a TransactionAbortedException, or log the failure.
writer.WriteLine("Transaction rolled back.");
}
// Display messages.
Console.WriteLine(writer.ToString());
return returnValue;
}
}