こんにちは!Access VBAの世界へようこそ。
システム開発の現場で、こんな恐怖のメッセージに直面したことはありませんか?
> 「他のユーザーが同じデータを開いています。この変更を保存しますか?」
マルチユーザー環境のAccessデータベースにおいて、この「書き込み競合(ロック競合)」は避けて通れない宿命のようなものです。特に、VBAからパパッと動的SQLや`QueryDef`を使ってデータを一括更新しようとした矢先にこれが発生すると、プログラムは無慈悲に止まってしまいますよね。
「マクロやVBAの自動化を組んだはいいけれど、現場でエラーが頻発して使い物にならない……」
そんな壁にぶつかっているあなたへ。今回は、DAOのエラーコードを巧みに読み解き、裏でこっそり自動リトライしてくれる「不屈のQueryDef実行ルーチン」の作り方を、優しく、そして本質的なところまで徹底解説します。
ここをクリアすれば、あなたの作ったAccessアプリは一気に「プロの現場で耐えうる堅牢なシステム」へと生まれ変わりますよ。さあ、一緒に扉を開けましょう!
—
1. なぜ「書き込み競合」は起きるのか?(基本のキ)
Accessは、ファイル共有型のデータベース(ACE/Jetエンジン)です。Excelのように「開いたもん勝ち」ではなく、レコードを更新する瞬間に「レコードロック」という安全装置が働きます。
VBAの`QueryDef`を使って「さあ、データを更新するぞ!」とSQLを走らせたそのコンマ数秒のタイミングで、別のユーザー(あるいは別のフォーム)が同じレコードを触っていたとします。すると、Accessのエンジンはこう叫ぶのです。
「おいおい、どっちの変更を信じればいいんだ!衝突したから処理を中断する!」
これが書き込み競合の正体です。人間が操作しているなら「上書き保存するかキャンセルするか」を選べますが、VBAのコード実行中にこれが起きると、容赦なく実行時エラー(Runtime Error)として爆発します。
—
2. エラーを「回避」するのではなく「いなして再挑戦」する
初学者のうちは、「エラーが出たらどうしよう」とビクビクしてしまいますが、ベテランエンジニアの発想は違います。
「マルチユーザー環境なんだから、競合は起きて当たり前。起きたら少し待って、もう一回やり直せばいいじゃない」
そう、これこそが今回目指す「自動リトライ設計」の核心です。
具体的には、以下のステップでコードを組みます。
1. `QueryDef`でSQLを実行する。
2. もしエラーが起きず成功したら、何事もなかったかのように終了。
3. エラーが起きたら、エラー番号が「書き込み競合(またはロック関連)」のものかチェックする。
4. 対象のエラーであれば、少しだけ処理を止める(ウェイトを入れる)。
5. あらかじめ決めた回数(例:3回や5回)に達するまで、1〜4をループしてリトライする。
これだけで、現場からの「またシステムが止まったんだけど!」というクレームを劇的に減らすことができます。
—
3. 実装コード:魂の自動リトライ・QueryDef実行関数
それでは、実際の開発現場でそのままコピペして使える実用的なコードをお見せします。
標準モジュールに貼り付けてご利用ください。
Option Explicit
‘ =========================================================================
‘ 指定したQueryDef(またはSQL)を、競合リトライ機能付きで実行するプロシージャ
‘ =========================================================================
Public Sub ExecuteQueryWithRetry(ByVal qd As DAO.QueryDef, Optional ByVal MaxRetryCount As Long = 3)
Dim retryCount As Long
Dim success As Boolean
retryCount = 0
success = False
Do While Not success And retryCount < MaxRetryCount
On Error GoTo ErrorHandler
' --- 処理の本体:QueryDefの実行 ---
' ※今回はアクションクエリ(UPDATEやINSERTなど)を想定しています
qd.Execute dbFailOnError
' エラーなくここまで来たら成功フラグを立ててループを抜ける
success = True
On Error GoTo 0
Exit Do
ErrorHandler:
' DAOのエラー番号を解析
' 3186: テーブルをロックできませんでした(別ユーザーが使用中)
' 3218: データベースはロックされています
' 3260: 競合しています。他のユーザーがデータを更新中です
Select Case Err.Number
Case 3186, 3218, 3260
retryCount = retryCount + 1
If retryCount > MaxRetryCount Then
‘ リトライ上限オーバー時はエラーを上位に投げる
MsgBox “他のユーザーによるデータ競合が頻発しているため、処理を中断しました。” & vbCrLf & _
“しばらく待ってから再度実行してください。”, vbCritical, “競合エラー(上限到達)”
Err.Raise Err.Number, “ExecuteQueryWithRetry”, Err.Description
End If
‘ — 待機処理(ウェイト) —
‘ 0.5秒〜1秒程度待つことで、他のユーザーの処理が終わるのを期待する
‘ ※Access VBAにはSleepがないため、Timer関数で簡易ウェイトを作ります
Call WaitSeconds(1.0)
‘ ループの先頭に戻ってリトライ
Resume Next
Case Else
‘ 競合以外の予期せぬエラーはそのままスローする
Dim errDesc As String
errDesc = Err.Description
On Error GoTo 0
Err.Raise Err.Number, “ExecuteQueryWithRetry”, errDesc
End Select
Loop
End Sub
‘ =========================================================================
‘ 指定した秒数だけ処理を一時停止するヘルパー関数
‘ =========================================================================
Private Sub WaitSeconds(ByVal seconds As Single)
Dim startTime As Single
startTime = Timer
Do While Timer < startTime + seconds ' 画面が固まらないようにイベントを許可する DoEvents ' 日付をまたいだときの対策(深夜0時を跨ぐ場合のケア) If Timer < startTime Then startTime = Timer End If Loop End Sub ---
4. このコードのスマートな使い方(呼び出し例)
先ほど作成した堅牢な関数を、実際の業務ロジックからどのように呼び出すのかを見てみましょう。ここでは、パラメータクエリを使った動的SQLの更新を例にします。
Public Sub UpdateCustomerStatus()
Dim db As DAO.Database
Dim qd As DAO.QueryDef
Set db = CurrentDb
On Error GoTo ProcError
‘ あらかじめデザインビューで作ってあるQueryDefオブジェクトを取得
Set qd = db.QueryDefs(“qryUpdateCustomerStatus”)
‘ 動的パラメーターの設定
qd.Parameters(“prmStatus”) = “VIP”
qd.Parameters(“prmTargetDate”) = Date – 30
‘ ★ここで先ほど作った「自動リトライ関数」を呼び出す!
Call ExecuteQueryWithRetry(qd, 5) ‘ 最大5回までリトライする設定
MsgBox “ステータスの更新が正常に完了しました!”, vbInformation, “完了”
ProcError:
If Err.Number <> 0 Then
MsgBox “エラーが発生しました: ” & Err.Description, vbCritical, “エラー”
End If
‘ オブジェクトの解放(メモリリークを防ぐプロの作法)
If Not qd Is Nothing Then qd.Close: Set qd = Nothing
Set db = Nothing
End Sub
—
5. チーフアーキテクトからのワンポイントアドバイス
ここまでのコードを理解できたあなたなら、もう初級者から一歩抜け出しています。最後に、現場でさらに一歩進んだ設計にするための知見を共有します。
1. トランザクションとの組み合わせに注意する
`BeginTrans` と `CommitTrans` の中でこのリトライ関数を呼び出す場合、トランザクション自体がロックを保持したままになるため、リトライの意味が薄れることがあります。競合が起きやすい重い一括処理は、トランザクションを細かく区切るか、今回のように単体のアクションクエリ単位でリトライさせるのが安全です。
2. 「待機時間(バックオフ)」をランダムに散らすのが本当のプロ
複数の端末が一斉に同じタイミングでデータを更新しようとして競合した場合、全員が全く同じ「1秒待機」をしてリトライすると、「リトライの瞬間」にまた全員で一斉にぶつかる(これを「雷雨効果」と呼びます)現象が起きることがあります。
本番環境の極限状態を目指すなら、`WaitSeconds` の時間を `Rnd 1.5` のように少しランダムに揺らしてやると、競合の確率はさらに劇的に下がります。
—
まとめ
Access VBAにおける「書き込み競合」は、初心者が最初に絶望する壁の一つです。しかし、エラーコード(3186, 3218, 3260)を正しくキャッチし、適切な待機を挟んでリトライする設計を取り入れるだけで、アプリの信頼性はプロ仕様へと劇的に跳ね上がります。
「エラーを恐れるな、エラーを飼いならせ。」
この精神を持てば、Accessデータベース開発はもっと楽しく、もっと自由になりますよ。
ここをクリアしたあなたなら、どんな現場でも通用する素晴らしいエンジニアになれます。ぜひ、あなたのシステムにもこの「リトライの知恵」を取り入れてみてくださいね!
