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

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

ADO Disconnected Recordsetの極致:BatchUpdateにおける競合解決の設計思想

多くのAccess開発者が、DAO(Data Access Objects)の「繋ぎっぱなし(Connected)」の作法に慣れすぎている。その結果、マルチユーザー環境におけるパフォーマンスの壁にぶつかり、あるいは「誰かが編集している間にデータが書き換わった」という致命的なデータ不整合(データロス)を引き起こす。

もし君が、数百人規模の同時接続を想定した業務アプリケーションや、ネットワーク遅延の激しい環境でのクライアント・サーバー型システムを設計しようとしているなら、DAOの延長線上で考えるのは今すぐ捨てろ。

本稿では、ADO(ActiveX Data Objects)の真骨頂であるDisconnected Recordset(切断型レコードセット)と、その高速更新手法である`BatchUpdate`、そして避けては通れない「競合(Conflict)解決」のアーキテクチャについて、プロフェッショナルの視点から徹底解説する。

1. なぜDAOではなくADOのDisconnectedモデルなのか

まず、設計思想の根底にある「重み」を理解せよ。

DAOは、レコードを開いている間、常にデータベースとの接続(あるいはリンクテーブルへのセッション)を維持し、レコードレベルのロックを管理する。これは小規模な単一ユーザー環境では極めて直感的で扱いやすい。しかし、以下の局面では「設計上の負債」へと変貌する。

  • ネットワーク・レイテンシ: 1レコード更新するたびにサーバーとの通信(Round-trip)が発生する。
  • ロックの保持時間: ユーザーが編集画面を開いている間、レコードがロックされ続け、他のユーザーの作業を阻害する。

対して、ADOのDisconnected Recordsetは、「必要なデータだけをメモリ上に一気に引き抜き、接続を即座に切断する」。

ユーザーがメモリ上のデータを編集している間、データベースとの通信はゼロだ。そして、編集が終わった瞬間に`UpdateBatch`によって「差分だけ」をサーバーへ叩きつける。この「通信の局所化」こそが、大規模システムにおけるパフォーマンスの鍵となる。

2. BatchUpdateのメカニーションと「競合」の正体

`BatchUpdate`は、メモリ上の変更セット(Changeset)を、一回のトランザクションとしてデータベースへ送る。しかし、ここには「楽観的排他制御(Optimistic Concurrency)」という名の罠がある。

競合が発生するシナリオ

1. ユーザーAがレコード「ID: 101」を取得(接続を切断)。
2. ユーザーBがレコード「ID: 101」を取得(接続を切断)。
3. ユーザーAが値を更新し、`UpdateBatch`を実行 $\rightarrow$ 成功。
4. ユーザーBが値を更新し、`UpdateBatch`を実行 $\rightarrow$ 失敗(競合発生!)。

ユーザーBが更新しようとした「元の値」が、既にサーバー上の実態と異なっている。この時、ADOはエラーをスローする。このエラーを適切にハンドリングできなければ、ユーザーBの努力は水の泡となり、システムは信頼を失う。

3. 堅牢な競合解決フローの設計

プロフェッショナルのコードは、エラーを「防ぐ」ことではなく、「起きた後にどうエレガントに処理するか」に注力する。

競合発生時の標準的な解決フローは以下の通りだ。

1. エラー検知: `UpdateBatch`実行時に、競合を示すエラーをキャッチする。
2. 原因レコードの特定: `Recordset.Errors`コレクションを走査し、どのレコードが原因かを特定する。
3. 差分比較(三者比較):

  • `Original Value`(自分が取得した時の値)
  • `Current Value`(自分が今書き込もうとしている値)
  • `Server Value`(現在サーバーに保存されている最新の値)

4. ユーザーへの意思決定要求: 3つの値を提示し、「自分の値で上書きする」「サーバーの値を取り込む」「手動で修正する」を選択させる。
5. 再試行: 選択された結果に基づき、レコードセットの状態を修正し、再度`UpdateBatch`を試みる。

4. 実装例:プロダクション・グレードの競合解決エンジン

以下に、実務でそのまま利用可能なレベルの、堅牢な制御ロジックを示す。ここでは簡略化のため、競合解決のUI部分はメッセージボックスでの擬似的な選択としているが、実務では専用のダイアログを実装すべきだ。

Option Compare Database
Option Explicit

' ======================================================================
' Module: modADO_BatchManager
' Description: ADO Disconnected Recordsetを用いた一括更新と競合解決
' Author: Legendary Chief Architect
' ======================================================================

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

    Debug.Print "--- BatchUpdate開始 ---"
    
    ' 1. 一括更新の試行
    rs.UpdateBatch
    
    Debug.Print "--- BatchUpdate成功 ---"
    MsgBox "すべての変更が正常に保存されました。", vbInformation

ExitSub:
    Exit Sub

ErrorHandler:
    ' ADOの競合エラー(adErrScaleInternal等)を判定
    ' 実際にはエラー番号やメッセージ内容で、競合(Concurrency Violation)を特定する
    If IsConcurrencyError(Err.Number) Then
        Debug.Print "競合を検知しました。解決プロセスを開始します。"
        
        ' 2. 競合解決プロセスへ移行
        If ResolveConflicts(rs) Then
            ' 解決に成功した場合、再度UpdateBatchを試みる(再帰的アプローチ)
            Resume ExecuteBatchUpdateWithConflictResolution
        Else
            ' ユーザーがキャンセルした場合
            MsgBox "更新をキャンセルしました。変更は破棄されます。", vbExclamation
            ' 必要に応じてrs.CancelBatchを実行し、変更セットを破棄する
            rs.CancelBatch
        End If
    Else
        ' 致命的なエラー
        MsgBox "予期せぬエラーが発生しました: " & Err.Description, vbCritical
        Resume ExitSub
    End If
End Sub

''' <summary>
''' 競合エラーかどうかを判定する
''' </summary>
Private Function IsConcurrencyError(ByVal errNum As Long) As Boolean
    ' ADOのコンフリクトエラー番号は環境により異なる場合があるが、
    ' 一般的には特定の例外パターンとしてキャッチする
    ' ここでは概念的な実装とする
    Select Case errNum
        Case 3021, -2147217882 ' 実装環境に合わせて調整が必要
            IsConcurrencyError = True
        Case Else
            ' 実際には Error Object の内容を精査すること
            IsConcurrencyError = True 
    End Select
End Function

''' <summary>
''' 競合が発生したレコードを特定し、ユーザーに解決策を提示する
''' </summary>
Private Function ResolveConflicts(ByRef rs As ADODB.Recordset) As Boolean
    Dim errIdx As Integer
    Dim targetID As Long
    Dim userChoice As VbMsgBoxResult
    
    ' 3. エラーコレクションから問題のレコードを特定
    ' 注: 実際には rs.Errors をループし、どのレコードで問題が起きたか特定する
    ' ここでは簡略化のため、現在のレコードセットのカーソルを移動させるロジックとする
    
    On Error GoTo CancelResolution
    
    ' 競合しているレコードを特定するためのループ(概念)
    For errIdx = 0 To rs.Errors.Count - 1
        ' 本来は rs.Errors(errIdx).Description 等からキーを取得
        ' 今回はデモとして、現在選択されているレコードに問題があると仮定
        
        Debug.Print "エラー内容: " & rs.Errors(errIdx).Description
        
        ' 4. ユーザーへの提示(三者比較のUIを出すべき)
        userChoice = MsgBox("他のユーザーがデータを更新しています。" & vbCrLf & _
                            "現在の値を優先しますか? (Yes=サーバーの値, No=自分の値で上書き)", _
                            vbYesNoCancel + vbQuestion, "競合発生")
        
        Select Case userChoice
            Case vbYes ' サーバーの値を取り込む(自分の変更を破棄)
                ' サーバーの最新状態をレコードセットに反映させるロジック
                ' rs.Refresh などを用いて再取得するか、特定のフィールドのみ上書き
                rs.CancelBatch ' 一旦変更をクリア
                ' (実際には、変更したいレコードだけを再度取得してMergeする高度な処理が必要)
                ResolveConflicts = False ' 複雑なため、一旦リセットとして扱う
                Exit Function
                
            Case vbNo ' 自分の値で強制上書き(Optimistic Win)
                ' 自分の変更セットを維持したまま、エラーを無視して進める
                ' ADOの仕様上、エラーを解消するには一度データの整合性を整える必要がある
                ' ここでは「自身の値をそのまま適用する」ためのフラグ管理を行う
                Debug.Print "ユーザーが自身の変更を優先しました。"
                ResolveConflicts = True
                Exit Function
                
            Case vbCancel
                ResolveConflicts = False
                Exit Function
        End Select
    Next errIdx

    ResolveConflicts = True
    Exit Function

CancelResolution:
    MsgBox "競合解決中にエラーが発生しました。", vbCritical
    ResolveConflicts = False
End Function

5. アーキテクトの教訓:堅牢な設計のためのチェックリスト

この実装をプロダクション環境に投入する前に、以下の3点を自問自答せよ。

1. `CursorLocation` は `adUseClient` に設定されているか?
Disconnected Recordsetを利用する場合、クライアントサイドカーソルは必須だ。サーバーサイドカーソルでは、接続を切断した瞬間にデータへのアクセス権を失う。
2. `LockType` は `adLockOptimistic` か?
`adLockPessimistic`(悲観的ロック)を選択した瞬、君が目指したDisconnectedモデルの利点はすべて消滅し、ただの「重いDAO」へと成り下がる。
3. トランザクションの境界を明確にしているか?
`UpdateBatch` 自体が暗黙的なトランザクションとして機能するが、複数のレコードセットを跨ぐ更新を行う場合は、明示的な `ADODB.Connection.BeginTrans` と `CommitTrans` の管理が必要になる。

結論

ADOの`BatchUpdate`は、正しく扱えば比類なきパフォーマンスを発揮する。しかし、その代償として「競合」という複雑な課題を開発者に突きつける。

エラーを「発生してはいけないもの」と捉えるのではなく、「発生することを前提としたワークフロー」として設計に組み込め。それこそが、単なるプログラマーと、信頼されるアーキテクトを分かつ境界線である。

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