境界線を越える:ADO Commandによる高並列更新と競合解決の極致
業務アプリケーションの規模が拡大し、マルチユーザー環境での同時更新が避けられなくなったとき、多くの開発者は「DAOの限界」という壁にぶち当たる。
DAO(Data Access Objects)は、Accessのエンジン(ACE/Jet)に直結した、いわば「内臓」のような存在だ。ローカル環境での単一ユーザー操作においては、その低レイテンシとシンプルさは無敵に近い。しかし、ネットワーク越しにSQL Serverや、あるいは分散された複数のAccess Backendへアクセスし、かつ「誰かが更新している間に、自分が書き込もうとした」という事態(Lost Update問題)に直面したとき、DAOの単純なロック機構では、システム全体のパフォーマンスを著しく低下させるか、あるいはデータの整合性を放棄するかの二択を迫られることになる。
ここで必要となるのが、ADO(ActiveX Data Objects)を用いた、オプティミスティック・コンカレンシー(楽観的同時実行制御)の設計である。
本稿では、単なる「データの読み書き」を超え、`ADODB.Command`オブジェクトを駆使した効率的なバッチ更新、および競合発生時のプロフェッショナルな解決フローの実装について、アーキテクチャの深淵から解説する。
1. アーキテクチャの断絶:DAO vs ADO
まず、設計思想の違いを脳裏に焼き付けておけ。
- DAO は「ファイルベース」の思考だ。レコードセットを直接操作し、エンジンが管理するロックを頼る。これは「一つの部屋に一人でいる」状態を前提としている。
- ADO は「プロトコルベース」の思考だ。OLE DBを介して、サーバーとの通信(Round-trip)をいかに制御し、いかにステートレスに、あるいは効率的なトランザクションで一連の操作を完結させるかに主眼が置かれる。
我々が目指すのは、後者だ。ネットワークの遅延(Latency)を最小化するために、個別の`Recordset.Update`を繰り返すのではなく、`Command`オブジェクトを用いて、パラメータ化されたクエリを、トランザクションの保護下で一括実行する。
2. 楽観的同時実行制御と「Version列」の戦略
競合を検知するためには、レコードが「いつ、誰によって変更されたか」を識別する証拠が必要だ。最も堅牢な手法は、テーブルに `RowVersion`(バイナリ型またはタイムスタンプ型)を持たせること、あるいは更新時に `LastModified`(日時型)を条件に含めることだ。
SQLの `WHERE` 句に、取得時のバージョン情報を組み込む。これにより、更新対象のレコードが既に他者に書き換えられていれば、`RecordsAffected` は `0` を返し、我々はそれを「競合」として検知できる。
3. 実装:Commandオブジェクトによる高効率更新
以下に、`ADODB.Command` を用いた、パラメータ化クエリによる更新の実装例を示す。文字列結合によるSQL構築は、SQLインジェクションの脆弱性を招くだけでなく、実行計画の再利用を妨げ、パフォーマンスを低下させる。プロフェッショナルは必ずパラメータを使用する。
' --- Module: modDataEngine ---
Option Explicit
' 構造体:競合解決のためのコンテキスト
Public Type ConflictContext
OriginalValue As Variant
CurrentServerValue As Variant
TargetID As Long
End Type
''' <summary>
''' パラメータ化クエリを用いた高効率な更新処理
''' </summary>
''' <param name="conn">接続済みConnectionオブジェクト</param>
''' <param name="targetID">更新対象のID</param>
''' <param name="newValue">更新後の値</param>
''' <param name="oldVersion">取得時に保持していたバージョン情報</param>
Public Function ExecuteSecureUpdate( _
ByRef conn As ADODB.Connection, _
ByVal targetID As Long, _
ByVal newValue As String, _
ByVal oldVersion As Date) As Boolean
Dim cmd As ADODB.Command
Dim rowsAffected As Long
' オブジェクトのライフサイクル管理:エラーハンドリングの徹底
On Error GoTo ErrorHandler
' Commandオブジェクトの生成
Set cmd = New ADODB.Command
With cmd
.ActiveConnection = conn
.CommandText = "UPDATE T_Inventory SET ItemName = ?, LastModified = ? " & _
"WHERE ID = ? AND LastModified = ?"
.CommandType = adCmdText
' パラメータの追加 (型とサイズを厳密に指定)
' ※ADOのパラメータ追加は、実行計画の最適化に寄与する
.Parameters.Append .CreateParameter("@Name", adVarChar, adParamInput, 100, newValue)
.Parameters.Append .CreateParameter("@ModDate", adDate, adParamInput, , Now())
.Parameters.Append .CreateParameter("@ID", adInteger, adParamInput, , targetID)
.Parameters.Append .CreateParameter("@OldDate", adDate, adParamInput, , oldVersion)
End With
' トランザクションの開始(一括処理を想定)
conn.BeginTrans
' 実行
cmd.Execute rowsAffected
' 競合検知のロジック
If rowsAffected = 0 Then
' 更新対象が見つからない、またはVersionが一致しない(=競合発生)
conn.RollbackTrans
ExecuteSecureUpdate = False ' 競合として返す
Else
conn.CommitTrans
ExecuteSecureUpdate = True
End If
CleanUp:
' メモリリーク防止のため、明示的にオブジェクトを解放
' これを怠ると、大規模ループ時にメモリ圧迫や接続リークを招く
Set cmd = Nothing
Exit Function
ErrorHandler:
If Not conn Is Nothing Then
If conn.State = adStateOpen Then conn.RollbackTrans
End If
Debug.Print "Critical Error: " & Err.Description
Resume CleanUp
End Function
4. 競合解決のUIフロー:ユーザーへの「選択」の委譲
競合が発生した際、システムが勝手に上書きすることは許されない。エンジニアリングの極意は、「データの整合性を守りつつ、ユーザーの作業をいかに妨げないか」にある。
競合検知時、以下のフローを実装する:
1. 現在のサーバー状態の再取得: 競合したレコードの最新データを取得する。
2. 差分表示: 「あなたの入力値」と「既に保存されている値」を比較提示する。
3. 解決策の提示: 「上書きする」「破棄して最新を取得する」「手動で修正して再送する」の3択を提供する。
' --- Form: frmEditItem ---
' 競合発生時のハンドリング例
Private Sub btnSave_Click()
Dim success As Boolean
Dim conflictInfo As ConflictContext
' 更新実行
success = modDataEngine.ExecuteSecureUpdate(g_conn, m_targetID, m_txtItemName.Text, m_originalDate)
If Not success Then
' 競合発生時の処理
If MsgBox("他のユーザーによってデータが更新されました。現在の値を表示して解決しますか?", _
vbExclamation + vbYesNo, "競合検知") = vbYes Then
' 1. 最新データの取得(ここでは簡略化して記述)
Dim latestVal As String
latestVal = GetLatestValueFromServer(m_targetID)
' 2. ユーザーへの提示(UI上での比較)
' 本来は専用のダイアログで「新旧比較」を行うのがベストプラクティス
m_txtItemName.Text = latestVal
m_originalDate = GetLatestDateFromServer(m_targetID)
MsgBox "最新の状態を読み込みました。内容を確認して再度保存してください。", vbInformation
End If
Else
MsgBox "更新に成功しました。", vbInformation
End If
End Sub
5. 極限の知見:メモリ管理とWindows APIの視点
シニアエンジニアとして、忘れてはならない領域がある。それは、VBAが動いているのはWindows OSの上にある、という事実だ。
オブジェクトの解放とCOMの挙動
ADOのオブジェクト(`Connection`, `Command`, `Recordset`)はCOM(Component Object Model)ベースである。VBAのスコープを抜けても、参照カウントが残っている限り、メモリ上に居座り続け、DB接続を掴みっぱなしにする。これは、サーバー側のコネクションプールを枯渇させ、システム全体のデッドロックを引き起こす。
`Set obj = Nothing` は、単なる作法ではない。リソース管理の責務である。
パフォーマンスの重み
大量のデータを一括更新する場合、`Command`オブジェクトをループ内で生成し直すのは愚策だ。`Parameters` の値だけを書き換えて、同じ `Command` オブジェクトを再利用せよ。これにより、COMオブジェクトの生成コストと、DBエンジン側の解析コストを劇的に削減できる。
結論
ADOによる一括更新と競合解決は、Accessアプリケーションを「単なるツール」から「エンタープライズ・システム」へと昇華させるための必須技術だ。
`Command`オブジェクトによるパラメータ化、`RowsAffected`による検知、そして厳格なトランザクション管理。これらを組み合わせることで、データ整合性とパフォーマンスの高度な両立が可能となる。
設計段階で「競合は必ず起こる」という前提に立ち、その解決策をアーキテクチャに組み込んでおけ。それが、真のアーキテクトの仕事である。
