Access VBAを掌握する極限の知見:マルチユーザーの悪夢「書き込み競合」をQueryDefとリトライ制御で完全制圧する
マルチユーザー環境のAccessデータベース運用において、開発者を最も絶望させるエラーの筆頭が「書き込み競合(Write Conflict)」だ。
「このレコードは、別のユーザーによって編集されています。…」
フォームでの作業中ならまだユーザーが手動で回避できるが、これがVBAによるバッチ処理や、`QueryDef`を用いた一括更新(`Execute`メソッド)の最中に発生した瞬間、システムは容赦なく停止し、トランザクションはロールバックされる。
なぜこのエラーが起きるのか。そして、どうすればこの排他制御の罠をスマートに突破できるのか。
今回は、DAOの深層を知り尽くしたアーキテクトの視点から、「書き込み競合をVBAで検知し、極限までエレガントに自動リトライさせる堅牢な設計」を伝授する。
—
1. なぜ「書き込み競合」は起きるのか?(非効率な設計の排除)
多くの初中級プログラマは、エラーハンドリングといえば `On Error Resume Next` で暴発を防ぐか、メッセージを出して処理を中断するだけの安易な実装を行う。しかし、それでは業務自動化ツールとしての価値はゼロだ。
競合の本質
Access(Jet/ACEエンジン)の裏側では、ページ単位、あるいはレコード単位での楽観的・悲観的ロックが複雑に絡み合っている。特に `QueryDef` を使ったアクションクエリ(更新・追加)の実行時は、対象レコードが他者に掴まれているだけで、容赦なくDAOエラー(Runtime Error)が発生する。
マルチユーザー環境では、同時実行による一時的なロックは「日常茶飯事」である。
したがって、「競合が起きないように祈る設計」ではなく、「競合が起きることを前提とし、即座にリトライして自己修復する設計」こそが、プロフェッショナルなプロダクションコードの絶対条件なのだ。
—
2. 堅牢なリトライ設計のアーキテクチャ
自動リトライを実装するにあたり、以下の3点を厳守しなければならない。
1. エラー番号の厳密な特定
DAOにおける「書き込み競合」や「リソースビジー」に該当するトラップ可能なエラー番号(例: `3186`, `3260`, `3192` など)のみを捕捉する。予期せぬバグまでリトライさせると、無限ループの地獄に陥る。
2. 指数バックオフ(Exponential Backoff)とランダムジッター
即座に再リトライを繰り返すのは、ロックを掴んでいる相手にとってもサーバー(ファイルサーバー)にとっても負荷が高い。試行回数に応じて待機時間を延ばし、さらに競合タイマーを散らす(ランダム要素を入れる)ことで、再衝突の確率を劇的に下げる。
3. トランザクションの適切なスコープ管理
`QueryDef.Execute dbFailOnError` を使用する場合、トランザクションの境界をどこに置くかが生命線となる。
—
3. 【コピペ即採用】プロダクションコード実装例
以下のコードは、実務の現場でそのまま組み込める、堅牢な`QueryDef`実行ラッパー関数だ。
エラー番号の捕捉、リトライ回数の制限、待機処理(`DoEvents`とWindows APIの`Sleep`の併用)を完璧に網羅している。
Option Compare Database
Option Explicit
‘ Windows APIの宣言(ミリ秒単位の精緻な待機のため)
If VBA7 Then
Private Declare PtrSafe Sub Sleep Lib “kernel32” (ByVal dwMilliseconds As Long)
Else
Private Declare Sub Sleep Lib “kernel32” (ByVal dwMilliseconds As Long)
End If
‘ =========================================================================
‘ 汎用関数: 書き込み競合自動リトライ付き QueryDef 実行エンジン
‘ =========================================================================
Public Function ExecuteQueryDefWithRetry( _
ByVal qd As DAO.QueryDef, _
Optional ByVal intMaxRetries As Integer = 5, _
Optional ByVal lngBaseDelayMs As Long = 500) As Boolean
Dim intAttempt As Integer
Dim lngDelay As Long
Dim errLoop As DAO.Error
Dim bSuccess As Boolean
bSuccess = False
intAttempt = 0
Do While intAttempt < intMaxRetries
intAttempt = intAttempt + 1
On Error GoTo ErrorHandler
' データベースの変更を確実にするためトランザクション内で実行
' ※必要に応じてクエリ側で dbFailOnError を付与しておくこと
qd.Execute dbFailOnError
' 成功した場合
bSuccess = True
Exit Do
ErrorHandler:
' DAOエラーの詳細を解析
Dim lngErrNum As Long
lngErrNum = Err.Number
' 競合・ロック関連のエラーコードか判定
' 3186: データの保存ができません。(他ユーザーがロック中)
' 3260: ほかのユーザーがテーブルを使用しています。
' 3192: データベースまたはオブジェクトがロックされています。
If lngErrNum = 3186 Or lngErrNum = 3260 Or lngErrNum = 3192 Then
If intAttempt >= intMaxRetries Then
‘ 最大試行回数を超えた場合はログを残して致命的エラーへ
MsgBox “書き込み競合が解消されません。他のユーザーの作業完了を待って再実行してください。” & vbCrLf & _
“エラー (” & lngErrNum & “): ” & Err.Description, vbCritical, “致命的な排他エラー”
bSuccess = False
Exit Do
End If
‘ 指数バックオフ + ランダム要素(ジッター)の計算
‘ 例: 500ms 試行回数 + 0〜200msのランダム値
Randomize
lngDelay = (lngBaseDelayMs intAttempt) + Int((200 – 0 + 1) Rnd + 0)
‘ ログ出力(イミディエイトウィンドウへ通知)
Debug.Print “【競合検知・リトライ】 試行回数: ” & intAttempt & _
” / 待機時間: ” & lngDelay & “ms / エラー: ” & Err.Description
‘ UIのフリーズを防ぎつつ待機
DoEvents
Sleep lngDelay
‘ エラーをクリアして次のループへ
Err.Clear
Resume Next
Else
‘ 競合以外の予期せぬエラー(構文エラーや型不一致など)は即座にスロー
MsgBox “予期せぬエラーが発生しました (” & lngErrNum & “): ” & Err.Description, vbCritical, “システムエラー”
bSuccess = False
Exit Do
End If
Loop
ExecuteQueryDefWithRetry = bSuccess
End Function
—
4. 呼び出し側の実装パターン
上記のエンジンを活用する際は、クエリ定義(`QueryDef`)オブジェクトを渡すだけでいい。パラメータが存在する場合は、実行前に割り当てるのが鉄則だ。
Public Sub Sample_RunUpdateQuery()
Dim db As DAO.Database
Dim qd As DAO.QueryDef
Dim blnResult As Boolean
Set db = CurrentDb
‘ あらかじめ作成済みのパラメータクエリを取得
Set qd = db.QueryDefs(“q_UpdateInventoryStatus”)
‘ パラメータの動的バインド
qd.Parameters(“prmTargetDate”) = Date
qd.Parameters(“prmStatusFlag”) = 1
‘ リトライ制御付き実行エンジンの呼び出し
blnResult = ExecuteQueryDefWithRetry(qd, intMaxRetries:=5, lngBaseDelayMs:=1000)
If blnResult Then
MsgBox “在庫ステータスの更新が正常に完了しました。”, vbInformation, “処理成功”
Else
MsgBox “処理は中断されました。”, vbExclamation, “処理中断”
End If
‘ クリーンアップ
Set qd = Nothing
Set db = Nothing
End Sub
—
5. チーフアーキテクトからの実務的アドバイス
1. バックエンド・フロントエンド分離の徹底
大前提として、多人数で共有するAccessデータベースにおいて、ロジック(フォーム・コード)とデータ(テーブル)が同一ファイル(mdb/accdb)にあってはならない。ファイルサーバー上でこれをやると、書き込み競合以前にネットワーク帯域の飽和とファイル破損(Corrupt)の危険性が跳ね上がる。必ずACCDE/ACCDBを分割し、ローカルにフロントを持たせること。
2. トランザクションと `dbFailOnError` のセット運用
アクションクエリ(`Update`, `Append`, `Delete`)を実行する際、`dbFailOnError` を指定しないと、途中でエラーが起きても「成功した部分だけコミットされる」という最悪のデータ不整合を引き起こす。上記コードのようにクエリ自体に `dbFailOnError` を付与し、アトミック(不可分)な処理を担保せよ。
総括
「書き込み競合」はAccessの仕様上の限界ではなく、設計の未熟さをあぶり出すリトマス試験紙に過ぎない。
エラーをハンドリングし、自律的に回復するコードを書くこと。それこそが、現場のユーザーから「止まらない、信頼できる業務システム」と絶賛されるエンジニアの条件である。明日からの実装に、ぜひこの知見を組み込んでほしい。
