【テクニカル・上級編】ADOのBatchUpdateによる一括更新と、競合発生時の手動解決フロー – Access VBA解析バイブル

スポンサーリンク
スポンサーリンク

データの整合性とパフォーマンスの極北:ADO BatchUpdateによる一括更新と競合解決の設計論

業務システムを設計する際、多くの開発者が「正常系」の構築に全精力を注ぎ、システムの命運を分ける「異常系」、とりわけマルチユーザー環境下におけるデータ競合(Concurrency Conflict)の処理を軽視する。

DAO(Data Access Objects)によるローカルなレコード操作に慣れきったエンジニアが、SQL Server等のRDBMSをバックエンドに据えたADO(ActiveX Data Objects)のパラダイムに触れたとき、必ずと言っていいほど直面する壁がある。それが、BatchUpdateによる一括更新と、発生した競合への事後対応だ。

本稿では、単なるコードの書き方ではなく、オブジェクトのライフサイクル、メモリ管理、そしてデータ整合性を担保するためのアーキテクチャ設計という観点から、ADO BatchUpdateの真髄を解説する。

1. アーキテクチャの断絶:DAOとADOの決定的な違い

まず、我々が立ち向かうべき敵の正体を知る必要がある。

DAOは、Accessのエンジンに最適化された「レコード単位」の操作を前提としている。ロックの粒度は細かく、直感的だ。しかし、ネットワーク越しに大規模なRDBMSを操作する場合、レコードごとに通信(ラウンドトリップ)を発生させるDAOの手法は、ネットワーク帯域を食いつぶし、システム全体のパフォーマンスを著しく低下させる。

一方で、ADOのBatchUpdateは、クライアント側のメモリ上に「更新の意図(Change Set)」を蓄積し、一度の通信でサーバーへ送りつける。これは極めて効率的だが、「クライアントが編集している間に、他の誰かがサーバー上のデータを書き換えていた場合」、一括更新は致命的なエラーを吐いて停止する。

この「楽観的排他制御(Optimistic Concurrency)」における衝突を、いかにしてエレガントに、かつユーザーにストレスを与えずに解決するか。これこそがシニアエンジニアに課せられた責務である。

2. BatchUpdateを掌握するための必須条件

BatchUpdateを成功させるためには、以下の3つの条件を厳守しなければならない。これに反する実装は、実行時にランタイムエラーを招くだけのゴミコードとなる。

1. CursorLocation = adUseClient:
BatchUpdateはクライアントサイドカーソルが必須である。サーバーサイドカーソルでは、変更セットをクライアント側に保持できないため、一括更新のロジック自体が成立しない。
2. LockType = adLockOptimistic:
レコードを読み込んだ瞬間にロックをかけるのではなく、更新を試みた瞬間にロックをかける「楽観的ロック」を選択する。
3. 明示的なオブジェクト解放:
ADOオブジェクトはCOM(Component Object Model)に基づいている。特に大量のレコードを扱う場合、`Set rs = Nothing` による明示的な解放を怠れば、メモリリークは避けられない。

3. 実装の真髄:競合検知と手動解決フロー

以下に、競合が発生した際に、エラーコレクションを走査し、ユーザーに解決策(上書きするか、破棄するか)を提示する堅牢な実装パターンを示す。

'***
' Module: modDataEngine
' Description: ADO BatchUpdateを用いた高度な一括更新処理
'***
Option Explicit

Public Sub ExecuteBulkUpdateWithConflictResolution(ByRef rs As ADODB.Recordset)
    On Error GoTo ErrorHandler

    ' 1. BatchUpdateの実行を試みる
    ' このメソッドは、クライアント側の変更セットをサーバーへ一括送信する
    Debug.Print "BatchUpdateを開始します..."
    rs.UpdateBatch

    MsgBox "全レコードの更新が正常に完了しました。", vbInformation
    Exit Sub

ErrorHandler:
    ' ADOのエラーコード 3024 (adErrBatchUpdateError) は、一括更新中に競合が発生したことを示す
    If Err.Number = -2147467259 Or Err.Number = 3024 Then ' 実行時エラーおよびADO固有エラー
        Debug.Print "競合を検知しました。エラー解析を開始します。"
        HandleBatchUpdateError rs
    Else
        MsgBox "予期せぬエラーが発生しました: " & Err.Description, vbCritical
    End If
    
    ' オブジェクトの生存期間を管理するため、エラー発生時も適切に処理を中断させる
    Err.Clear
End Sub

'***
' Procedure: HandleBatchUpdateError
' Description: 競合発生時にエラーコレクションを走査し、ユーザーに解決を仰ぐ
'***
Private Sub HandleBatchUpdateError(ByRef rs As ADODB.Recordset)
    Dim i As Integer
    Dim conflictMsg As String
    Dim userChoice As VbMsgBoxResult
    
    ' ADOのErrorsコレクションを走査し、どのレコードで何が起きたかを確認する
    ' BatchUpdateエラーの場合、Errorsコレクションには衝突したレコードの情報が含まれる
    For i = 0 To rs.Errors.Count - 1
        conflictMsg = "【競合発生】" & vbCrLf & _
                      "エラー内容: " & rs.Errors(i).Description & vbCrLf & _
                      "発生箇所: レコードID " & rs.Errors(i).SQLState ' SQLState等で特定
        
        Debug.Print conflictMsg
        
        ' ユーザーへの通知(実務では、競合したレコードの「現在の値」と「自身の値」を比較提示すべき)
        userChoice = MsgBox(conflictMsg & vbCrLf & vbCrLf & _
                            "このレコードの変更を強制的に上書きしますか?" & vbCrLf & _
                            "(「いいえ」を選択すると、この変更は破棄されます)", _
                            vbYesNoCancel + vbExclamation, "データ競合")
        
        Select Case userChoice
            Case vbYes
                ' 強制上書きのロジック
                ' 本来は、ここで該当レコードを再度Fetchし、値を再セットして再試行する
                ' 簡略化のため、ここではログ出力に留めるが、実戦では「再取得→再適用」が必要
                Debug.Print "ユーザーにより上書きが選択されました。"
                
            Case vbNo
                ' 変更の破棄
                Debug.Print "ユーザーにより変更が破棄されました。"
                
            Case vbCancel
                ' 全体処理の中止
                Debug.Print "処理がキャンセルされました。"
                Exit Sub
        End Select
    Next i
    
    ' 最後に、Recordsetの内部状態をクリーンアップする必要がある
    ' 競合が発生したRecordsetは「不整合状態」にあるため、適切に処理しないと後続の操作が不可になる
    Debug.Print "エラー処理を完了しました。"
End Sub

4. アーキテクトの視点:メモリとパフォーマンスの最適化

この実装において、シニアエンジニアが意識すべきは、エラーが発生した際の「Recordsetの状態管理」である。

`UpdateBatch` が失敗したとき、Recordsetオブジェクトは「Dirty(汚染)」な状態にある。この状態で無理に他の操作を行おうとすれば、メモリ空間内で不整合が起き、Access自体がクラッシュするリスクがある。

メモリ最適化の極意

1. エラーコレクションの走査コスト:
`rs.Errors.Count` は、競合が発生した数に比例して増大する。数万件の更新で競合が多発するような設計は、そもそもアーキテクチャとして破綻している。競合は「稀に起こる例外」として設計すべきであり、メインフローに組み込んではならない。
2. 明示的なNothing代入:
VBAのガベージコレクションは、COMオブジェクトに対しては決して万能ではない。特に、`Connection` と `Recordset` が入れ子になっている場合、`Recordset` を先に `Nothing` にし、その後に `Connection` を `Nothing` にする。この順序を逆転させることは、メモリリークへの片道切符である。
3. Windows APIの活用(高度なメモリ監視):
もし、長時間稼働するバックエンドサービスとしてVBAを動かしているならば、`GetProcessMemoryInfo` 等のWindows APIを呼び出し、自プロセスのワーキングセット(Working Set)を監視する仕組みを検討せよ。これは、レガシーなシステムを現代のインフラで安定稼働させるための、プロフェッショナルの嗜みである。

結論

ADOのBatchUpdateは、強力な武器であるが、その取り扱いを誤ればシステムを崩壊させる諸刃の剣となる。

「エラーが起きたら止まる」という初歩的な実装を卒業し、「エラーが起きた後に、いかにしてデータの整合性を保ちつつ、ユーザーの意図を汲み取るか」を設計に組み込むこと。それが、Access VBAというレガシーな領域において、伝説的なアーキテクトとしての真価を問われる瞬間である。

タイトルとURLをコピーしました