【極限の設計】Access VBAにおける「ロック競合」を制する:再試行ロジック(Retry Pattern)の実装戦略
業務システムを開発していると、必ず直面する壁がある。それは「開発機では完璧に動いたコードが、多人数が利用する本番環境で突如としてエラーを吐き出す」という現象だ。
特に、Access VBAを用いた分散データベース環境において、DAO(Data Access Objects)のトランザクション処理中に発生する「ロック競合(Lock Contention)」は、開発者の設計レベルを試す残酷な試練となる。
多くの開発者は、エラーが発生した際に「エラーメッセージを表示して終了する」という、いわば「敗北宣言」を実装する。しかし、プロフェッショナルなアーキテクトが取るべき道は違う。競合を「避けられない一時的な事象」と定義し、システム自身に「賢明な再試行(Intelligent Retry)」を行わせる設計を組み込むことだ。
本稿では、DAOのトランザクションにおけるロックのメカニズムを解剖し、堅牢な「リトライ・パターン」の実装手法を伝授する。
1. なぜ「ロック競合」は発生し、なぜ単純なエラー処理では足りないのか
ロックの正体
Access(ACE/Jetエンジン)において、DAOを用いたトランザクション(`BeginTrans`)を開始すると、エンジンはデータの整合性を守るために、更新対象のレコードやページに対してロックをかける。
問題は、複数のユーザーが同時に同じテーブルの同じ領域を更新しようとした時だ。一方がトランザクションを保持している間、もう一方は「待機」を強いられる。この待機時間が、エンジンのタイムアウト設定を超えた瞬間に、我々が知るあの忌々しい「書き込み禁止」や「他のユーザーが使用中です」といったエラーが発生する。
「エラーを捕まえる」だけでは不十分な理由
一般的な `On Error GoTo ErrorHandler` だけの実装では、エラーが発生した瞬間に処理が中断される。しかし、ロック競合の多くは「一瞬の衝突」に過ぎない。コンマ数秒待てば、先行するトランザクションが完了し、後続の処理は成功する可能性が極めて高いのだ。
この「一瞬の隙」を逃さず、自動的にリトライさせる設計こそが、ユーザーにストレスを与えない「止まらないシステム」の条件である。
2. 堅牢な設計思想:Exponential Backoff(指数バックオフ)
単に「エラーが出たら即座に再試行する」という設計は、最悪の結果を招く。衝突が起きている最中に猛烈な勢いでリトライを繰り返せば、それは「リトライによる攻撃(Thundering Herd問題)」となり、データベースの負荷をさらに高め、デッドロックを悪化させる。
我々が採用すべきは、Exponential Backoff(指数バックオフ)という戦略だ。
1. 初回失敗時:短い待ち時間(例:0.1秒)を置いてリトライ。
2. 二回目失敗時:待ち時間を倍増(例:0.2秒)させる。
3. 三回目失敗時:さらに倍増(例:0.4秒)させる。
このように、失敗を重ねるごとに「待機時間を指数関数的に増やす」ことで、競合が解消されるための「時間的猶予」をシステムに与えるのだ。
3. プロダクション級の実装例
以下に、実務でそのまま利用できる、リトライロジックを組み込んだトランザクション制御のテンプレートを示す。これは単なるコードの断片ではなく、一つの「設計パターン」として理解してほしい。
共通モジュール:`modTransactionManager`
Option Compare Database
Option Explicit
' Windows APIの宣言(精密な待機時間を制御するため)
If VBA7 Then
Private Declare PtrSafe Sub Sleep Lib "kernel32" (ByVal dwMilliseconds As LongPtr)
Else
Private Declare Sub Sleep Lib "kernel32" (ByVal dwMilliseconds As Long)
End If
''' <summary>
''' トランザクション処理をリトライ制御付きで実行する。
''' </summary>
''' <param name="ExecuteProc">実行したい処理を記述したサブルーチン名(※実際にはコールバックが必要だが、VBAではパターン化して実装)</param>
''' <returns>成功時 True, 失敗時 False</returns>
Public Function ExecuteWithRetry(ByRef targetAction As ActionWrapper) As Boolean
Dim retryCount As Integer
Dim maxRetries As Integer
Dim waitTime As LongPtr
Dim success As Boolean
' --- 設定パラメータ ---
maxRetries = 5 ' 最大リトライ回数
waitTime = 100 ' 初回待機時間 (ミリ秒)
' ----------------------
retryCount = 0
success = False
Do While retryCount <= maxRetries
On Error GoTo ErrorHandler
' トランザクション開始
DBEngine.Workspaces(0).BeginTrans
' 実際の業務ロジックを実行
targetAction.Execute
' 全て成功したらコミット
DBEngine.Workspaces(0).CommitTrans
success = True
Exit Do
ErrorHandler:
' ロック競合に関連するエラー番号を判定
' 3200: 書き込み禁止、3186: 別のユーザーが使用中、等
' 3260: 複数のユーザーによるロック競合
If IsLockError(Err.Number) Then
' ロールバックしてリトライ準備
DBEngine.Workspaces(0).Rollback
retryCount = retryCount + 1
If retryCount <= maxRetries Then
' 指数バックオフ: 待機時間を倍々に増やしていく
Sleep waitTime
waitTime = waitTime * 2
Debug.Print "Lock Contention detected. Retry attempt: " & retryCount
Else
Debug.Print "Max retries reached. Aborting."
End If
Else
' ロック以外の致命的なエラー(構文エラー、型不一致等)は即座にロールバックして終了
DBEngine.Workspaces(0).Rollback
MsgBox "致命的なエラーが発生しました: " & Err.Description, vbCritical
Exit Do
End If
Loop
ExecuteWithRetry = success
Exit Function
End Function
''' <summary>
''' ロック競合に関連するエラーコードを判定する
''' </summary>
Private Function IsLockError(ByVal errNum As Long) As Boolean
Select Case errNum
Case 3021, 3022, 3033, 3186, 3200, 3260, 3261, 3264
IsLockError = True
Case Else
IsLockError = False
End Select
End Function
''' <summary>
''' 実行するロジックをカプセル化するためのクラス(概念)
''' </summary>
Public Class ActionWrapper
' 実務では、ここに処理内容を渡す仕組みを構築する
' VBAにはデリゲートがないため、インターフェースを模倣するか、
' 特定のメソッドを呼び出す設計にする
Public Sub Execute()
' 実際の業務ロジックはここから呼び出される
End Sub
End Class
実務での利用イメージ
VBAには「関数を引数として渡す」機能が乏しいため、実務では「処理内容をクラスまたは特定のメソッドに閉じ込める」設計が必要になる。
' 業務ロジックを実装したクラス
Public Class clsOrderProcessor
Implements ActionWrapper
Public Sub ProcessOrder()
' ここに実際のSQLやDAOの更新処理を書く
' 例: CurrentDb.Execute "UPDATE Orders SET Status = 'Processed' WHERE ID = 101", dbFailOnError
End Sub
' ActionWrapperインターフェースの実装
Private Sub ActionWrapper_Execute()
Me.ProcessOrder
End Sub
End Class
' --- 呼び出し側 ---
Public Sub RunOrderTask()
Dim processor As New clsOrderProcessor
Dim wrapper As ActionWrapper
Set wrapper = processor
If ExecuteWithRetry(wrapper) Then
MsgBox "処理が正常に完了しました。", vbInformation
Else
MsgBox "通信エラーまたはロック競合により処理に失敗しました。時間を置いて再試行してください。", vbExclamation
End If
End Sub
4. アーキテクトが守るべき「黄金律」
リトライロジックを組み込むことは重要だが、それはあくまで「最後の防衛線」だ。真のエンジニアは、そもそも競合が起きないような設計を行う。
1. トランザクションは「最短」に保て
`BeginTrans` から `CommitTrans` までの間に、ユーザーの入力を待ったり、重い計算を行ったりしてはならない。トランザクション内で行うのは「純粋なデータ操作」のみに限定すること。
2. 不必要なレコードロックを避ける
`Workspace.BeginTrans` は強力だが、その影響範囲は広い。可能な限り、更新が必要な最小限のレコードに対してのみ、適切なタイミングで更新を行う設計を心がけよ。
3. DAOとADOの使い分けを理解せよ
Accessのローカルテーブルを操作するならDAOが最も高速で、ロック制御も細かい。一方で、SQL Serverなどの外部サーバーへ接続する場合はADO(またはパススルークエリ)を使用し、ロックの責任をサーバー側に委ねるのが定石だ。
結論
Access VBAにおける堅牢なシステム開発とは、エラーが起きないことを祈ることではない。「エラーが起きることを前提とし、その影響をいかに最小化し、自動的に回復させるか」を設計することである。
今回紹介した「指数バックオフを用いたリトライパターン」をマスターすれば、あなたの作るツールは、単なる「便利な道具」から、プロフェッショナルが信頼を寄せる「堅牢な業務基盤」へと進化するだろう。
