競合を制する者、システムを制す:DAOトランザクションにおけるロック待ち回避と堅牢なリトライ・アーキテクチャ
マルチユーザー環境におけるAccessアプリケーションの開発において、最もエンジニアの精神を削る事象は何か?それはバグではない。「不定期に発生するロック競合による、ユーザーの操作停止」である。
単一ユーザーでのテストでは完璧に動作するコードが、実稼働環境で10人が同時に操作した瞬間に「実行時エラー 3260: 更新できません。現在ロックされています」を吐いて死ぬ。これは、アプリケーションの設計思想が「シーケンシャルな処理」に留まり、「並行性の制御」を甘く見ている証左である。
本稿では、DAO(Data Access Objects)を用いたトランザクション処理において、ロック待ちを検知し、指数バックオフ(Exponential Backoff)を用いたリトライ戦略を組み込むことで、極限まで堅牢性を高める実装手法を解説する。これは単なるエラーハンドリングではない。システムの生存戦略である。
1. DAOにおけるロックの正体とアーキテクチャの罠
ADO(ActiveX Data Objects)が汎用的なデータ接続を目的としているのに対し、DAOはJet/ACEエンジンに最適化された、いわば「ネイティブな操作」を提供する。この最適化こそが、高パフォーマンスを実現する一方で、ロック制御の難度を上げている。
Accessのマルチユーザー環境では、`.laccdb`ファイルを通じてレコードレベルまたはページレベルのロックが管理される。トランザクション(`BeginTrans`)を開始した瞬間、エンジンの整合性を守るためにロックの強度は増す。
ここで初心者が犯す致命的なミスは、「エラーが出たらRollbackして諦める」、あるいは「無限ループで待ち続ける」のどちらかである。前者はデータの整合性を破壊し、後者はユーザーのUIをフリーズさせる。我々が目指すべきは、「適切な間隔を空けて、スマートに再試行する」ことだ。
2. Windows APIを用いた精密な待機制御
VBA標準の`Pause`的な手法(`DoEvents`を回すなど)は、CPUリソースを無駄に消費し、スレッドの制御も不安定である。プロフェッショナルの実装には、Windows APIの`Sleep`を用いる。これにより、OSレベルでスレッドを待機状態に移行させ、システムへの負荷を最小限に抑えつつ、ミリ秒単位の精密な制御が可能になる。
3. 実装:堅牢なリトライ・トランザクション・パターン
以下に示すのは、ロック競合(Error 3260等)を検知した際に、自動的にリトライを行う実装例である。ここでは、単なる固定秒数の待機ではなく、リトライごとに待機時間を増やす「指数バックオフ」を採用している。これにより、競合が激しい状況下での「衝突の連鎖(Thundering Herd問題)」を防ぐ。
Option Compare Database
Option Explicit
' --- Windows API Declaration ---
If VBA7 Then
' 64bit環境用
Declare PtrSafe Sub Sleep Lib "kernel32" (ByVal dwMilliseconds As LongPtr) As Long
Else
' 32bit環境用
Declare Sub Sleep Lib "kernel32" (ByVal dwMilliseconds As Long) As Long
End If
' --- 定数定義 ---
Private Const MAX_RETRIES As Integer = 5 ' 最大リトライ回数
Private Const BASE_DELAY_MS As Long = 200 ' 初回待機時間(ms)
Private Const ERR_LOCK_CONFLICT As Long = 3260 ' レコードがロックされている
Private Const ERR_WRITE_CONFLICT As Long = 3211 ' 書き込み競合
''' <summary>
''' ロック競合を考慮したトランザクション実行エンジン
''' </summary>
Public Sub ExecuteSecureTransaction()
Dim db As DAO.Database
Dim ws As DAO.Workspace
Dim retryCount As Integer
Dim success As Boolean
Dim currentDelay As Long
Set db = CurrentDb
Set ws = DBEngine.Workspaces(0)
retryCount = 0
success = False
currentDelay = BASE_DELAY_MS
' リトライループの開始
Do While retryCount <= MAX_RETRIES And Not success
On Error GoTo ErrorHandler
' トランザクション開始
ws.BeginTrans
' --- 【実務ロジック領域】ここから業務処理を開始 ---
Call PerformBusinessLogic(db)
' --- 【実務ロジック領域】ここまで ---
' 全て成功したらコミット
ws.CommitTrans
success = True
Debug.Print "Transaction committed successfully."
Loop
If Not success Then
MsgBox "処理に失敗しました。サーバーの負荷が高いか、他のユーザーが長時間ロックしています。", vbCritical
End If
CleanUp:
' オブジェクトの明示的な解放(メモリリークと残留ロック防止の鉄則)
On Error Resume Next
Set db = Nothing
Set ws = Nothing
Exit Sub
ErrorHandler:
' ロック競合関連のエラーか判定
If Err.Number = ERR_LOCK_CONFLICT Or Err.Number = ERR_WRITE_CONFLICT Then
' トランザクションをロールバックして状態をリセット
ws.Rollback
retryCount = retryCount + 1
If retryCount <= MAX_RETRIES Then
Debug.Print "Lock conflict detected (Error " & Err.Number & "). Retry " & retryCount & "/" & MAX_RETRIES & " after " & currentDelay & "ms."
' 指数バックオフによる待機
Sleep currentDelay
currentDelay = currentDelay * 2 ' 待機時間を倍増させる
' エラーをクリアしてループ継続
Err.Clear
Resume Do
Else
Debug.Print "Max retries reached. Abandoning transaction."
GoTo CleanUp
End If
Else
' ロック以外の致命的なエラーは即座にロールバックして終了
ws.Rollback
MsgBox "致命的なエラーが発生しました: " & Err.Description, vbCritical
GoTo CleanUp
End If
End Sub
''' <summary>
''' 実際のデータ更新処理(サンプル)
''' </summary>
Private Sub PerformBusinessLogic(ByRef db As DAO.Database)
Dim rs As DAO.Recordset
' 例: 在庫テーブルの更新
Set rs = db.OpenRecordset("SELECT * FROM T_Inventory WHERE ProductID = 1", dbOpenDynaset, dbOptimistic)
If Not rs.EOF Then
rs.Edit
rs.Fields("StockCount").Value = rs.Fields("StockCount").Value - 1
rs.Update
End If
' Recordsetを即座に閉じる。これが不十分だとロックが残る原因になる。
rs.Close
Set rs = Nothing
End Sub
4. 伝説のアーキテクトが教える「極限の知見」
コードを書いただけでは不十分だ。以下の3点を血肉にせよ。
① オブジェクトのライフサイクル管理
`PerformBusinessLogic` 内で `Recordset` を開いた後、`rs.Close` および `Set rs = Nothing` を徹底しているか? トランザクション中にオブジェクトがメモリ上に浮遊していると、`Rollback` を実行してもエンジンが「まだ使用中」と判断し、ロックが解放されないケースがある。「使い終わったら即座に殺す」。これはVBAにおける生存本能である。
② 冪等性(Idempotency)の確保
リトライ処理を導入する場合、`PerformBusinessLogic` 内の処理は冪等(べきとう)でなければならない。つまり、「何度実行しても、結果が同じになる」ことだ。もし、関数の途中で外部システムへメールを送信するような処理が含まれていたら、リトライのたびにメールが飛んでしまう。トランザクションの範囲内には、「データの整合性に影響を与える純粋なデータ操作」のみを封じ込めよ。
③ トランザクションの粒度
トランザクションを長く保持すればするほど、ロック競合の確率は指数関数的に上昇する。
「全工程を一つのトランザクションにする」という安易な設計は、大規模環境では死を意味する。可能な限り、更新単位を最小限に分割し、一瞬でコミットを完了させる設計を追求せよ。
結論
Access VBAにおける堅牢なシステムとは、エラーが起きないシステムではない。エラーが起きた際、システムがいかに自律的に、かつユーザーにストレスを与えずに復旧できるか、その設計にこそエンジニアの真価が問われる。
今回提示したリトライ・アーキテクチャを基盤に、君たちのシステムを「止まらない、壊れない、妥協のない」ものへと昇華させてほしい。
