「ロック待ち」に屈しない堅牢なトランザクション設計:DAOにおけるリトライ・アーキテクチャの極意
業務アプリケーションの開発において、単に「動くコード」を書くことは容易だ。しかし、マルチユーザー環境、つまり複数の人間が同時に同じデータベースを叩く実戦の場で、「止まらない、壊れない、データが矛盾しない」システムを構築できるか。ここに、プロフェッショナルとアマチュアの決定的な境界線が存在する。
Access VBAを用いた開発において、多くの開発者が陥る罠がある。それは、DAO(Data Access Objects)のトランザクション処理における「ロック競合」の軽視だ。
本稿では、中級者への脱皮を目指すエンジニアに向けて、DAOのトランザクションにおけるロック待ちをいかに検知し、いかにエレガントに、かつ堅牢にリトライ(再試行)処理を実装すべきか、その設計思想と実装パターンを伝授する。
1. なぜあなたのトランザクションは「死ぬ」のか
まず、アーキテクチャの観点から整理しよう。ADO(ActiveX Data Objects)がデータベースエンジンを抽象化した「外側の窓口」であるのに対し、DAOはAccessの心臓部であるJet/ACEエンジンに直結した「内側の言語」だ。
DAOは極めて高速で、Access特有のオブジェクト操作に最適化されている。しかし、その分、エンジンの排他制御(Locking)の影響をダイレクトに受ける。
ロック競合のメカニズム
マルチユーザー環境で、ユーザーAが特定のレコードを更新し、トランザクションを完了(Commit)させていない状態で、ユーザーBが同じレコードを更新しようとすると、ACEエンジンはユーザーBの処理を「待機」させる。
通常のコードであれば、ここでプログラムは応答なし(フリーズ)の状態になるか、あるいは一定時間後に「実行時エラー 3260: 更新できません。現在ロックされています」を吐いて異常終了する。
「エラーが出たから、エラーハンドリングでメッセージを出して終わり」
もし君がそう考えているなら、今すぐその思考を捨ててほしい。それはユーザーの業務を中断させる「バグ」そのものだ。プロフェッショナルは、エラーを「予測される事象」として捉え、システム側で自動的に回復させる設計を行う。
2. 堅牢なリトライ・アーキテクチャの設計指針
ロック競合に対処するための設計には、以下の3つの要素が不可欠である。
1. 検知 (Detection): エラー番号(3260など)を特定し、それが「リトライ可能な一時的なエラー」であることを判別する。
2. 待機 (Backoff): ロックが解除されるのを待つ。このとき、即座にリトライするのではなく、わずかな「待ち時間」を設けることが重要だ。
3. 回数制限 (Threshold): 無限ループは厳禁だ。一定回数試行してもダメな場合は、致命的なエラーとしてロールバックし、ユーザーに通知する。
ここで、単なる「一定時間の待機」ではなく、「指数バックオフ(Exponential Backoff)」の概念を少し取り入れると、より高度な設計になる。リトライを繰り返すごとに待ち時間を少しずつ伸ばすことで、サーバー(あるいはファイル共有)への負荷を抑えつつ、成功率を高める手法だ。
3. 実装:プロダクション・レディなリトライ・ラッパー
以下に、実務でそのまま利用可能な、高信頼性のトランザクション実行パターンを示す。このコードは、ビジネスロジックを「コールバック関数」のように渡すのではなく、実務上扱いやすい「手続き型リトライ構造」として設計している。
Option Compare Database
Option Explicit
' Windows APIのSleep関数を宣言(待機処理の精度を高めるため)
If VBA7 Then
Declare PtrSafe Sub Sleep Lib "kernel32" (ByVal dwMilliseconds As LongPtr)
Else
Declare Sub Sleep Lib "kernel32" (ByVal dwMilliseconds As Long)
End If
''' <summary>
''' 堅牢なトランザクション実行プロシージャ
''' </summary>
Public Sub ExecuteRobustTransaction()
Dim db As DAO.Database
Set db = CurrentDb
' --- 設定値 ---
Const MAX_RETRIES As Integer = 5 ' 最大リトライ回数
Const INITIAL_WAIT_MS As Long = 500 ' 初回の待機時間 (0.5秒)
' --------------
Dim retryCount As Integer
Dim isSuccess As Boolean
Dim currentWait As Long
retryCount = 0
isSuccess = False
currentWait = INITIAL_WAIT_MS
' リトライループの開始
Do While retryCount <= MAX_RETRIES And Not isSuccess
On Error GoTo ErrorHandler
' 1. トランザクション開始
DBEngine.BeginTrans
' 2. ビジネスロジックの実行(ここに実際の更新処理を記述)
' ※本来はここを別関数に切り出すのが理想的
Call PerformBusinessLogic(db)
' 3. コミット
DBEngine.CommitTrans
' 成功時
isSuccess = True
Debug.Print "Transaction committed successfully."
Loop Exit Do ' ループ脱出
CleanExit:
Set db = Nothing
Exit Sub
ErrorHandler:
' エラー番号 3260: Could not update; currently locked
' エラー番号 3211: Could not update; currently locked (別のエラーパターン)
If Err.Number = 3260 Or Err.Number = 3211 Then
' ロック競合が発生した場合
retryCount = retryCount + 1
' 未完了のトランザクションがあればロールバック
On Error Resume Next
DBEngine.Rollback
On Error GoTo 0
If retryCount <= MAX_RETRIES Then
Debug.Print "Lock contention detected. Retry attempt: " & retryCount & " after " & currentWait & "ms"
' 指定時間待機
Sleep currentWait
' 指数バックオフ: 次回の待ち時間を増やす (例: 1.5倍)
currentWait = Int(currentWait * 1.5)
' 次のループへ
Resume Next
Else
' 最大リトライ回数を超過した場合
MsgBox "サーバーが混雑しているため、処理を完了できませんでした。しばらく待ってから再試行してください。", vbCritical, "システムエラー"
Debug.Print "Max retries reached. Transaction failed."
Resume CleanExit
End If
Else
' ロック以外の致命的なエラーが発生した場合
DBEngine.Rollback
MsgBox "予期せぬエラーが発生しました: " & Err.Description, vbCritical, "致命的エラー"
Debug.Print "Critical Error: " & Err.Number & " - " & Err.Description
Resume CleanExit
End If
End Sub
''' <summary>
''' 実際のデータ操作を行うロジック(分離して記述)
''' </summary>
Private Sub PerformBusinessLogic(ByRef db As DAO.Database)
' サンプル用のデータ更新処理
Dim rs As DAO.Recordset
Set rs = db.OpenRecordset("T_Inventory", dbOpenDynaset, dbSeeChanges)
If Not rs.EOF Then
rs.Edit
rs!StockCount = rs!StockCount - 1 ' 在庫を減らす
rs.Update
End If
rs.Close
Set rs = Nothing
End Sub
4. アーキテクトからの補足:設計上の注意点
このコードを実装するにあたり、以下の3点を必ず肝に銘じておいてほしい。
① ロールバックの徹底
エラーが発生した際、`DBEngine.Rollback` を呼び出さずにループを継続してはならない。中途半端な状態のトランザクションが残ったまま次の試行に移ると、データベースの状態は予測不能なものになり、最悪の場合、ファイル全体が破損する。
② `dbSeeChanges` の利用
もし、接続先が SQL Server などの分散トランザクションを伴う環境(AccessからSQL Serverへ接続している場合)であれば、`OpenRecordset` の引数に `dbSeeChanges` を忘れないこと。これがないと、自動増分型(Identity)の列を持つテーブルに対して更新を行う際にエラーとなる。
③ 粒度の小さいトランザクション
トランザクションの範囲(`BeginTrans` から `CommitTrans` まで)を長くしすぎてはいけない。
「大量のデータを一括更新する」ために、数万件のループを一つのトランザクションで囲むのは、ロック競合を意図的に引き起こしているようなものだ。「一連の整合性を保つ最小単位」を見極め、トランザクションを細かく分割する設計を心がけよ。
結論
Access VBAにおける堅牢なシステム構築とは、エラーが起きないように祈ることではない。「エラーが起きることを前提に、いかにシステムが自律的に回復できるか」を設計することだ。
今回示したリトライ・アーキテクチャを導入することで、君のツールは「使いやすい道具」から「信頼に値する業務基盤」へと進化するだろう。
