【実務・中級編】ADOのCommandオブジェクトによる「一括更新(BatchUpdate)」と「競合発生時の手動解決」の実践 – Access VBA解析バイブル

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

【極限の知見】ADO BatchUpdateによる一括更新と、実務で不可避な「競合解決」の設計論

「とりあえず、ループの中でRecordsetを回して `.Update` すればいいでしょう?」

もし君が、現場のリードエンジニアに対してそんな言葉を口にするつもりなら、今すぐその思考を捨ててほしい。それは「動くプログラム」を作っているだけで、「堅牢なシステム」を作っているわけではない。

特に、複数ユーザーが同時に同じデータに触れるエンタープライズな業務アプリケーションにおいて、その「甘い設計」は、ある日突然、データの不整合という名の時限爆弾となって君のプロジェクトを破壊する。

今日は、Access VBAにおけるデータアクセスの極致、ADO(ActiveX Data Objects)を用いた「Disconnected Recordset(切断型レコードセット)」による一括更新(BatchUpdate)、そして避けては通れない「競合発生時の手動解決フロー」の設計について、その真髄を伝授しよう。

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

まず、君が「なぜDAOではなくADOなのか」を論理的に説明できるようになっておく必要がある。

Access開発において、DAO(Data Access Objects)は標準的だ。ACEエンジンに最適化されており、単一ユーザーのローカル操作には極めて高速だ。しかし、DAOは基本的に「常にデータベースエンジンと接続している」ことを前提とした「接続型(Connected)」のアーキテクチャだ。

対して、ADOは「切断型(Disconnected)」の運用が可能だ。

なぜ「切断型」が必要なのか?

1. ネットワーク負荷の最小化: データをメモリ上にバッファとして持ち、操作が終わったら即座に接続を切る。
2. 一括処理(BatchUpdate): メモリ上で行った大量の変更を、一つのトランザクションとして、あるいは一つのコマンドとしてサーバー(エンジン)に叩き込むことができる。
3. 楽観的ロック(Optimistic Locking)の制御: 「データを読み込んだ時」と「書き込む時」の間に、他者が書き換えていないかを検証するロジックを、アプリケーション層で高度に制御できる。

実務で「多人数が同時に更新する」という要件が出た場合、DAOの単純な更新では、他者の更新を「上書き」してしまうか、あるいは「エラーで即終了」するかの二択になりがちだ。これを回避するのが、ADOによる高度な競合制御である。

2. BatchUpdateのメカニズム:Client-Side Cursorの重要性

ADOで一括更新を実現するためには、`CursorLocation` プロパティを `adUseClient` に設定することが絶対条件だ。

' これが設計の肝となる設定だ
rs.CursorLocation = adUseClient

これを指定することで、レコードセットの実体はデータベースエンジンではなく、クライアント側のメモリ上に展開される。この「メモリ上のコピー」に対して、ユーザーは自由に編集を加える。そして、最後に `UpdateBatch` メソッドを呼び出した瞬間、ADOは「変更があったレコード」だけを特定し、一括でデータベースへ同期を試みる。

この時、ADOは内部的に「元の値(Original Value)」を保持している。更新時に「元の値が、現在のDBの値と一致するか?」をチェックすることで、他者による割り込みを検知する仕組みだ。

3. 実践:競合解決を含む堅牢な実装パターン

では、具体的なコードを見ていこう。単なるコピペ用コードではない。エラーハンドリング、トランザクション、そして競合発生時の「ユーザーへの問いかけ」までを考慮した、プロダクションクオリティの設計だ。

プロダクション・コード例:BatchUpdateコンポーネント

このコードは、標準モジュール、あるいはクラスモジュールに実装することを想定している。

Option Explicit

' ADOの定数定義(参照設定していない場合のため)
Private Const adUseClient As Long = 3
Private Const adCmdText As Long = 1
Private Const adErrorsOccurred As Long = 4 ' 概念的なエラー判定用

''' <summary>
''' データをメモリ上に展開し、編集可能な状態にする
''' </summary>
Public Function LoadDisconnectedRecordset(ByVal strSQL As String) As ADODB.Recordset
    Dim rs As New ADODB.Recordset
    
    ' 【極限の知見】Client-side Cursorを指定しなければ、BatchUpdateは不可能だ
    rs.CursorLocation = adUseClient
    
    ' Openメソッドでデータをメモリにロード
    rs.Open strSQL, CurrentProject.Connection, adOpenStatic, adLockOptimistic
    
    Set LoadDisconnectedRecordset = rs
End Function

''' <summary>
''' メモリ上の変更をDBへ一括反映する。競合が発生した場合は解決フローへ移行する。
''' </summary>
Public Sub SaveChangesWithConflictResolution(ByRef rs As ADODB.Recordset)
    On Error GoTo ErrorHandler

    ' 1. まずは一括更新を試みる
    rs.UpdateBatch
    
    MsgBox "全ての変更が正常に保存されました。", vbInformation
    Exit Sub

ErrorHandler:
    ' ADOのエラーコレクションを解析する
    ' 競合(Optimistic Locking Failure)が発生した場合、ここに飛ぶ
    Dim errObj As ADODB.Error
    Dim isConflict As Boolean: isConflict = False
    
    For Each errObj In rs.Errors
        ' エラー内容から競合を判定(プロバイダーによりメッセージは異なるが、
        ' 一般的に「値が変更されている」旨のエラーをキャッチする)
        Debug.Print "Error: " & errObj.Description
        isConflict = True
    Next errObj

    If isConflict Then
        ' 2. 競合発生時の解決フローを開始
        HandleConflict rs
    Else
        MsgBox "予期せぬエラーが発生しました: " & Err.Description, vbCritical
    End If
    Exit Sub
End Sub

''' <summary>
''' 競合発生時に、ユーザーに「自分の変更」か「他者の変更」かを選択させる
''' </summary>
Private Sub HandleConflict(ByRef rs As ADODB.Recordset)
    ' 本来はここで、競合したレコードを特定し、UI(フォーム)を表示する
    ' ここではロジックの骨組みを示す
    
    Dim response As VbMsgBoxResult
    response = MsgBox("他のユーザーがデータを更新しています。" & vbCrLf & _
               "自身の変更を強制的に適用しますか?" & vbCrLf & _
               "(「いいえ」を選択すると、最新のデータを再読み込みします)", _
               vbYesNoCancel + vbExclamation, "競合検知")

    Select Case response
        Case vbYes
            ' 【強制上書き戦略】
            ' 競合を解消するため、一度レコードセットを最新状態に同期し、
            ' 再度自身の変更を適用する高度な処理が必要
            Debug.Log "強制上書き処理を実行..."
            ' ※実際には、rs.Requery 等で最新状態を取り込み、
            ' 変更箇所だけを再度セットし直す実装を行う
            
        Case vbNo
            ' 【破棄・再ロード戦略】
            ' 自身の変更を捨て、DBの最新状態をロードする
            Debug.Log "最新データを再ロードします。"
            rs.Requery
            MsgBox "最新の状態に更新されました。再度編集してください。", vbInformation
            
        Case vbCancel
            ' 【中断】
            Debug.Log "処理を中断しました。"
    End Select
End Sub

4. 設計における「落とし穴」と回避策

このアーキテクチャを実装する際、君が絶対に陥ってはならない罠が3つある。

① 「全レコード更新」の罠

`UpdateBatch` は、レコードセット内の「変更があったレコード」のみを対象とする。しかし、もし君が `rs.Requery` を不用意に呼び出すと、メモリ上の変更(Dirtyな状態)はすべて消滅する。競合解決の際、「どこまでがユーザーの変更で、どこがDBの最新値か」を管理する変数を別途保持しておく設計が、真にプロフェッショナルな実装だ。

② トランザクションの境界線

`UpdateBatch` 自体が内部的にトランザクションとして振る舞うが、複数のテーブルを跨ぐ更新を行う場合は、`ADODB.Connection` レベルでの `BeginTrans` と `CommitTrans` を組み合わせる必要がある。ただし、切断型レコードセットを使用している場合、接続が切れているタイミングでは `BeginTrans` は効かない。「接続している間に、一括更新の準備を整える」という順序を徹底せよ。

③ UI/UXの欠如

コードが完璧でも、ユーザーへの提示が「エラー番号 3022」のような無機質なものなら、そのシステムは失敗だ。競合が発生した際、「何が、誰によって、どう変わったのか」を比較表示する画面を用意すること。これが、現場のユーザーから「このツールは信頼できる」という評価を得るための、エンジニアとしての誠実さだ。

結論:エンジニアとしての視座

「動けばいい」という思考は、開発コストを下げているようでいて、実は運用コストと技術負債を爆発的に増大させている。

ADOの `BatchUpdate` と競合解決をマスターするということは、単に技術を習得することではない。「データの一貫性(Consistency)」という、データベースにおける最も神聖な原則を守るための戦術を身につけることなのだ。

次にコードを書くときは、自問自答してほしい。
「もし、今この瞬間に別のユーザーがデータを書き換えたら、私のプログラムはどう振る舞うのか?」と。

その答えが、君を凡庸なプログラマから、伝説的なアーキテクトへと引き上げる。

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