ロジックの定義
「Unify.aspx.vb」にあり、プロシージャ構成は前回とまったく同じです。
ただし、今回はトランザクション処理を利用する名前空間のインポート宣言として、冒頭の部分に「Imports System.Transactions」と記述する必要があります。
また、前回と異なるのは「btnUnify_Click」プロシージャのみで、「トランザクション処理」と「関連テーブルの更新」のコードを追加します。
ASP.NETでトランザクション処理を行う方法の一つとしてSqlTransactionを用いる「マニュアルトランザクション」があり、コーディングは以下のようになります。
Try sqlcon.Open() 'データベースの接続 'トランザクションの開始 Dim sqltran As SqlTransaction = sqlcon.BeginTransaction(IsolationLevel.Serializable) 'SQLコマンドの実行 sqlcmd = New SqlCommand("UPDATE Member ~", sqlcon) '(1)SQLコマンドの生成 sqlcmd.Transaction = sqltran '(2)SQLコマンドをトランザクションに参加させる Dim affectedRow as Integer = sqlcmd.ExecuteNonQuery() '(3)SQLコマンドの実行 If affectedRow = 0 Then '(4)実行結果が失敗だった場合 sqltran.Rollback() 'ロールバック End If '上記(1)~(4)をSQLコマンドの数だけ繰り返す sqltran.Commit() 'ここまで来たら、すべてのSQLコマンドが成功したのでコミット Finally sqlcon.Close() '接続を閉じる End Try
もう一つの方法として、TransactionScopeを用いる「自動トランザクション」があります。
今回は以下の理由により、「自動トランザクション」による実装例を紹介します。
- TransactionScopeで囲むだけの、シンプルなコーディングが可能なため
- お使いのシステムへの実装で分散トランザクション(後述)が必要になった場合、マニュアルトランザクションでは対応できなくなるため
'テーブルの更新処理 Try Using scope As New TransactionScope 'トランザクション処理 Using SqlConnection As New SqlConnection(constr) '(会員テーブルの更新) '(1)新規レコードの追加 '(2)新しい会員IDの取得 '(3)旧レコードの更新(楽観的同時実行制御) '(関連テーブルの更新) '(4)名寄せ先対象会員の利用サービスIDの取得 '(5)関連テーブルの名寄せ先の処理 '(6)関連テーブル名寄せ元の処理 End Using If lblErrMsg.Text = "" Then scope.Complete() '(7)コミット処理 End Using Catch ex As TransactionAbortedException lblErrMsg.Text &= "データベースの更新に失敗しました。" Catch ex As Exception lblErrMsg.Text &= "処理に失敗しました。<br />" & ex.Message & "<br />" & strSQL End Try
トランザクション処理は、SystemTransaction名前空間のTransactionScopeクラスでRDB操作全体を囲むだけで実現できます。具体的には「Using scope As New TransactionScope」と「End Using」で囲んだ間が一連のトランザクションと見なされます。操作がすべて成功した場合は、Completeメソッドによりコミットできます。
システムエラー発生時は自動的にロールバックが行われ、TransactionAbortedException例外が発生しますので、Try~Catch命令で捕捉して必要な処理を行います。
また、業務エラーが発生した場合も、(7)のようにTransactionScope 内でCompleteメソッドを実行しないことにより、自動的にロールバックが行われます。
会員テーブルの更新部分は前回とまったく同じです。処理イメージを再掲しておきます。
ただし、「(3)旧レコードの更新(楽観的同時実行制御)」部分のUPDATE文では、WHERE句の条件にMemberIDだけでなく、すべてのフィールドが、元のデータと同じであることを追加しています。また、名寄せ処理が他のユーザにより行われた場合も考慮し、Status='A'の条件(これによりUnifiedIDのチェックは不要)も追加しています。
UPDATE文は旧会員IDの1レコードごとに行い、実行により影響を受けた行数が1でなかったら、他のユーザによりデータが更新されたことになります。この場合業務エラーとしlblErrMsg.Textにメッセージを設定し、(7)でCompleteメソッドを実行しないことにより、ロールバックが行われるようにしています。
'会員テーブル名寄せ元の処理(楽観的同時実行制御) 'UPDATE文の会員共通部分の文字列を組み立てる strSQL = "UPDATE Member SET Status='U', UnifiedID=" & newMemberID For col As Integer = 0 To oldMemberID.Length - 1 '会員数だけ繰り返す 'UPDATE文のWHERE句の条件を組み立てる Dim strWHERE As String = " WHERE MemberID=" & oldMemberID(col) '会員ID条件 For row As Integer = 1 To aryField.Length - 1 '会員ID以外のデータの条件を追加 If data(row, col) <> "'NULL'" Then strWHERE &= " AND (" & aryField(row) & "=" & data(row, col) & ")" Else strWHERE &= " AND (" & aryField(row) & " IS NULL)" End If Next strWHERE &= " AND Status='A'" '名寄せ前である条件を追加 sqlcmd = New SqlCommand(strSQL & strWHERE, sqlcon) Dim affectedRows As Integer = sqlcmd.ExecuteNonQuery() If affectedRows <> 1 Then 'UPDATE文が失敗した場合 lblErrMsg.Text &= "会員ID:" & oldMemberID(col) & "のデータが他ユーザにより更新されたので、処理を中断します。<br />" End If Next
関連テーブルの更新部分は、(4)利用していたサービスIDを配列に格納し、(5)新しい会員IDとサービスIDを持つ新規レコードを関連テーブルに追加し、(6)関連テーブルの元のレコードを名寄せ済みとして更新します。
なお、関連テーブルについては、読み出しから書き込みの間にユーザとの対話時間がなく、瞬時に終了するものと考え、楽観的同時実行制御は省略しています。
分散トランザクションが必要になる場合
System.Transactions名前空間では、通常はローカルトランザクションとして動作しますが、必要に応じて分散トランザクションに昇格(守備範囲の拡大)します。
「ローカル」と「分散」トランザクションの違いは、「RDBが1台のサーバーのみにあり、複数の同時接続がない」か「RDBが複数サーバーにまたがるか、複数の同時接続がある」かの違いです。
今回のサンプルでは1つのRDB、1つの接続なのでローカルトランザクションとして動作しますので心配はありませんが、お使いのシステムに組み込んだ際「サーバー '~' のMSDTCは使用できません」といったエラーが起こる場合は、分散トランザクションが必要な状況になっています。例えば、関連テーブルの更新処理が別のRDBに分散している、関数化(モジュール化)されている中でRDB接続を開いている、といった場合が考えられます。
このような場合、分散トランザクションを制御するMS-DTC(Microsoft Distributed Transaction Coordinator)サービスが稼働している必要がありますので、サーバーの[管理ツール]から[サービス]を起動し、サービス一覧の中から、「Distributed Transaction Coordinator」を[開始]させてください。
まとめ
3回にわたり、名寄せに必要な処理を一通り紹介しました。
- (1)SQLによる重複値の抽出
- (2)泣き別れデータの統合
- (3)関連テーブルの更新
(1)では同姓同名データのEXISTS句による鮮やかな抽出、(2)ではASP.NETにおける動的なTableコントロールの扱い、(3)では関連するテーブルの処理とトランザクション、排他制御について取り上げました。皆様がお使いの会員管理や顧客管理のRDBシステムに、名寄せ機能を組み込む指針となれば幸いです。