■分散トランザクションとMSDTC
ここではまず、分散トランザクションとMSDTCについて説明しておきます。
実はTransactionScopeクラスによる自動トランザクションは非常に高機能で、1台のデータベース・サーバでのトランザクション(「ローカル・トランザクション」と呼びます)だけでなく、複数のデータベース・サーバにまたがるトランザクション(=分散トランザクション)にも対応しています。
分散トランザクションの例として、A支社からB支社への送金処理を本社から行う場合を考えてみましょう。ここで送金処理とは、実際に現金を送るのではなくて、一方のデータベースの口座情報をマイナスし、他方のデータベースの口座情報をプラスするものとします。
このような状況では、出金処理と入金処理を1つのトランザクションにしておく必要があります。入金処理が失敗したら、出金処理を取り消さなくてはならないためです。データベースが分散しているので、これは特に分散トランザクションと呼ばれます。
分散トランザクションでは、ローカル・トランザクションに比べて、特別な処理が必要になります。例えば、出金処理が完了したとしても、A支社ではデータベースを実際に更新することができません。それを行うには、B支社の更新が成功するということが確定している必要があるためです。
そこで次のような処理が行われます。本社はそれぞれの支社のデータベースを更新していきますが、各支社では実際には更新せずに、更新を保留しておき、更新が成功することを本社に伝えます。そして本社では、すべての支社で更新が成功するということを確認できた場合にのみ、各支社に更新の実施を依頼します。この依頼を受けて、各支社は保留しておいた更新を実際に行います。
このように、トランザクションを行っている本社は、支社のデータベース・システムと連絡を取りながら処理を進める必要があるわけですが、その調整を担当するのが分散トランザクションのコーディネイタであるMSDTCサービスです(もちろんデータベース・システムもMSDTCサービスに対応している必要があります。SQL Serverは対応しています)。
ちなみに、このような、すべての更新が成功することを確認した後に、実際に更新を行うコミットは「2相コミット」あるいは英語のまま「ツーフェイズ・コミット(two-phase commit)」と呼ばれます。
.NETの自動トランザクションによる分散トランザクションは、2相コミットという手法を使って実現されており、それを行うにはMSDTCサービスが必要になるというわけです。
■テーブルアダプタによる更新と分散トランザクション
それではなぜ、先ほどの更新処理が、MSDTCサービスの必要な分散トランザクションとして認識されたのでしょうか。それは、トランザクション処理の中で、データベースとの接続を複数回行っているためです。自動トランザクションでは、接続が1つだけならローカル・トランザクションとして処理されますが、接続が2つ以上になると、自動的に分散トランザクションとして処理されるという「昇格可能トランザクション」と呼ばれる機能があります。
ここで「接続が1つだけ」というのは、接続を示すオブジェクトであるSqlConnectionオブジェクトが、トランザクション処理の中で1つだけしか存在していない場合という意味です。
今回のプログラムでは、例えば削除処理では、次のように2つのテーブルアダプタのUpdateメソッドを呼び出しています。
Me.注文明細TableAdapter.Update(rows)
Me.注文TableAdapter.Update(rows)
結論だけをいうと、各Updateメソッド呼び出しでは、個別にデータベースへの接続が行われており*4、少なくとも異なる2つのSqlConnectionオブジェクトが使用されます。そのため同じデータベースにしかアクセスしていないにもかかわらず、2番目のUpdateメソッドで分散トランザクションに昇格してしまい、MSDTCが見つからずエラーとなるわけです。
*4 接続はコネクション・プールによりキャッシュされており、接続時に使用される接続文字列が同一であれば、同じ接続が再利用されるため、毎回接続を開き直しても問題にはなりません。
ということで、2つのテーブルアダプタで同一の接続(SqlConnectionオブジェクト)を使用するようにすれば、この問題は解決します。
幸いにして、テーブルアダプタは、それが使用するSqlConnectionオブジェクトを、Connectionプロパティとして公開しています。これを使って、トランザクションの開始直後に次のような2行を挿入します。
' 接続をオープンし、その接続のみを使うように設定
Me.注文明細TableAdapter.Connection.Open()
Me.注文TableAdapter.Connection = Me.注文明細TableAdapter.Connection
このコードでは、明示的に注文明細テーブルアダプタの接続をオープンし、それを注文テーブルアダプタでも使用するように設定しています。
通常、テーブルアダプタのUpdateメソッドは自分で接続をオープンし、更新が終わったら自分でクローズしますが、すでに接続がオープンしていれば、そのまま更新を行い、クローズも行わないという仕様になっています。上記のコードが有効なのは、このためです。
自分で接続をオープンしたので、クローズするのも忘れてはいけません。そうしないと、2度目に[データの保存]ボタンがクリックされたときに、オープン済みの接続をさらにオープンしようとしてエラーとなってしまいます。
さて、以上をまとめると、[データの保存]ボタンのイベント・ハンドラは最終的に次のようになります。
Private Sub 注文BindingNavigatorSaveItem_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles 注文BindingNavigatorSaveItem.Click
Me.Validate()
Me.注文BindingSource.EndEdit()
Me.注文明細BindingSource.EndEdit()
Dim rows() As DataRow
' トランザクションの始まり
Using scope As New System.Transactions.TransactionScope()
' 接続をオープンし、その接続のみを使うように設定
Me.注文明細TableAdapter.Connection.Open()
Me.注文TableAdapter.Connection _
= Me.注文明細TableAdapter.Connection
' 注文明細テーブルの削除処理
rows = Me.NORTHWNDDataSet.注文明細.Select( _
"", "", DataViewRowState.Deleted)
If rows.Length <> 0 Then
Me.注文明細TableAdapter.Update(rows)
End If
' 注文テーブルの削除処理
rows = Me.NORTHWNDDataSet.注文.Select( _
"", "", DataViewRowState.Deleted)
If rows.Length <> 0 Then
Me.注文TableAdapter.Update(rows)
End If
' 注文テーブルの追加・変更処理
rows = Me.NORTHWNDDataSet.注文.Select("", "", _
DataViewRowState.Added Or DataViewRowState.ModifiedCurrent)
If rows.Length <> 0 Then
Me.注文TableAdapter.Update(rows)
End If
' 注文明細テーブルの追加・変更処理
rows = Me.NORTHWNDDataSet.注文明細.Select("", "", _
DataViewRowState.Added Or DataViewRowState.ModifiedCurrent)
If rows.Length <> 0 Then
Me.注文明細TableAdapter.Update(rows)
End If
' 接続をクローズ
Me.注文明細TableAdapter.Connection.Close()
scope.Complete() ' トランザクションのコミット
End Using ' トランザクションの終わり
End Sub
それでは最後にテストしてみましょう。分かりやすいように、再度、データベース・ファイルをbin\Debugディレクトリにコピーしてから行います。
まずコミットする場合です。アプリケーションを起動して、注文データを1件削除し、データの保存を行います。そしてアプリケーションを再起動してみましょう。データの件数が829件となり、最初の1件が削除されていればOKです。
次にロールバックされる場合です。データベースの不整合を確認したときと同様に、コードの途中にブレイクポイントを設定してから(図4参照)、削除、保存を行い、プログラムを途中で中断します。再度アプリケーションを起動したとき、削除したはずの注文データがそのまま残っていれば、トランザクション処理が正しく行われたといえます。
以上、8回にわたってお送りしてきた「Visual Studio 2005によるWindowsデータベース・プログラミング」ですが、今回が最終回となります。
しかし、のんびり連載している間に、Visual Studio 2008が登場してしまいました。Visual Studio 2008でも、本連載で解説してきた内容はほぼ有効なようですが、UpdateAllというメソッドを持ったTableAdapterManagerクラスが自動生成されるなど、追加されている機能もいくつかあるようです。また、LINQや、LINQ to SQLのためのO/Rマッピング機能が導入されるなど、新たなデータベース・アクセス手法も見え始めています。これらについては、連載をあらためて解説していく予定です。
Copyright© Digital Advantage Corp. All Rights Reserved.