【実務・中級編】QueryDef実行時に発生する「書き込み競合」をVBAで検知し、自動リトライさせる堅牢な設計 – Access VBA解析バイブル

スポンサーリンク

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の仕様上の限界ではなく、設計の未熟さをあぶり出すリトマス試験紙に過ぎない。
エラーをハンドリングし、自律的に回復するコードを書くこと。それこそが、現場のユーザーから「止まらない、信頼できる業務システム」と絶賛されるエンジニアの条件である。明日からの実装に、ぜひこの知見を組み込んでほしい。

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