【実務・中級編】DAOのトランザクションで発生する「ロック競合」を解消するタイムアウト制御の設計 – Access VBA解析バイブル

スポンサーリンク
スポンサーリンク

【極限の設計】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における堅牢なシステム開発とは、エラーが起きないことを祈ることではない。「エラーが起きることを前提とし、その影響をいかに最小化し、自動的に回復させるか」を設計することである。

今回紹介した「指数バックオフを用いたリトライパターン」をマスターすれば、あなたの作るツールは、単なる「便利な道具」から、プロフェッショナルが信頼を寄せる「堅牢な業務基盤」へと進化するだろう。

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