【実務・中級編】DAOのトランザクションでハマる「ロック待ち」:デッドロックを回避するトランザクション設計 – Access VBA解析バイブル

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

DAOのトランザクションでハマる「ロック待ち」:デッドロックを回避するトランザクション設計

開発現場でよくある悲劇がある。
「開発環境では完璧に動作した。しかし、ユーザーが複数人になった途端、システムが『応答なし』になったり、特定の操作でフリーズしたりする」

もし君が、エラーメッセージすら出ない「無言のフリーズ」に直面しているなら、原因はほぼ間違いなくDAOのトランザクション設計における排他制御の誤解だ。

多くの開発者は `BeginTrans` と `CommitTrans` を単なる「一連の処理をまとめる魔法の呪文」だと思っている。だが、アーキテクトの視点は違う。トランザクションとは、「データベースのリソースを占有し、他者のアクセスを拒絶する宣言」なのだ。

本記事では、Access VBAにおけるDAOのトランザクションが引き起こす「ロック待ち」と「デッドロック」のメカニズムを解剖し、プロフェッショナルが実践する「堅牢なトランザクション設計パターン」を伝授する。

1. なぜ君のコードは「フリーズ」するのか?

ロックの正体

DAOでトランザクションを開始すると、その中で更新(Update)したレコードに対して「ロック」がかかる。このロックは、`CommitTrans` または `Rollback` が実行されるまで解除されない。

問題は、「トランザクションの範囲(スコープ)を広げすぎていること」にある。

アンチパターン:肥大化したトランザクション

以下のようなコードを書いていないか?

' --- 典型的なダメな設計例 ---
Public Sub BadTransactionExample()
    Dim db As DAO.Database
    Set db = CurrentDb
    
    On Error GoTo Err_Handler
    db.BeginTrans ' トランザクション開始
    
    ' 1. 重たい計算処理(数秒かかる)
    Dim result As Double
    result = HeavyCalculation() 
    
    ' 2. ユーザーへの確認(ここでユーザーが「キャンセル」を押すまでロックが続く!)
    If MsgBox("更新しますか?", vbYesNo) = vbNo Then GoTo Err_Handler
    
    ' 3. データの更新
    db.Execute "UPDATE T_Inventory SET Stock = Stock - 1 WHERE ID = 1", dbFailOnError
    
    db.CommitTrans ' ここでようやくロック解放
    Exit Sub

Err_Handler:
    db.Rollback
    MsgBox "Error: " & Err.Description
End Sub

なぜこれが致命的なのか?
1. 計算処理の長さ: `HeavyCalculation` が実行されている間、すでにデータベースのリソースへの準備が始まっている場合がある。
2. ユーザーの介入: 最悪なのは `MsgBox` だ。ユーザーが昼休憩に出かけてしまったら? その間、対象のレコードは他の全ユーザーからロックされ、システム全体が停止する。

これが「ロック待ち」の正体だ。

2. 鉄則:トランザクションの「最小化」と「分離」

プロフェッショナルな設計における鉄則はただ一つ。
「計算(Logic)と書き込み(I/O)を完全に分離せよ」だ。

トランザクションの範囲内には、「SQLの発行(書き込み)」のみを含めるべきである。データの準備、複雑な計算、外部ファイルとの照合などは、すべてトランザクションの外側で完了させておく。

設計の3原則

1. Prepare Phase (準備フェーズ): トランザクション外で、書き込むべきデータをメモリ上(配列やコレクション、あるいは変数)にすべて揃える。
2. Execute Phase (実行フェーズ): トランザクションを開始し、準備したデータを高速に書き込む。
3. Atomic Operation (原子性): 書き込みが成功したら即座に `Commit`、失敗したら即座に `Rollback` する。

3. プロダクションレベルの堅牢な実装パターン

では、実務でそのまま使える、保守性の高いコード構造を示そう。ここでは、計算処理と更新処理を分離したアーキテクチャを提示する。

' =================================================================
' Module: modInventoryManagement
' Description: 在庫更新処理(トランザクション設計のベストプラクティス)
' =================================================================

Option Explicit

' 更新用のデータ構造体
Private Type UpdatePayload
    ProductID As Long
    QuantityChange As Long
End Type

Public Sub ExecuteInventoryUpdate(ByVal targetID As Long, ByVal changeAmount As Long)
    Dim db As DAO.Database
    Dim payload As UpdatePayload
    
    On Error GoTo Err_Handler

    ' --- PHASE 1: Prepare (トランザクション外) ---
    ' ここで重たい計算やバリデーションを行う。
    ' ロックは一切かかっていない。
    payload.ProductID = targetID
    payload.QuantityChange = CalculateNewQuantity(changeAmount) ' 重たい計算
    
    If Not IsValidPayload(payload) Then
        Err.Raise vbObjectError + 513, "modInventoryManagement", "バリデーションエラー"
    End If

    ' --- PHASE 2: Execute (トランザクション内) ---
    ' 書き込み処理のみを極限まで短くまとめる。
    Set db = CurrentDb
    
    db.BeginTrans ' <--- ここでロック開始
    
    ' 最小限のSQL実行
    ' ※ 複数のテーブルを更新する場合も、ここですべて完結させる
    db.Execute "UPDATE T_Inventory SET Stock = Stock + " & payload.QuantityChange & _
               " WHERE ProductID = " & payload.ProductID, dbFailOnError
               
    db.Execute "INSERT INTO T_InventoryLog (ProductID, ChangeAmt, LogDate) " & _
               "VALUES (" & payload.ProductID & ", " & payload.QuantityChange & ", Now())", dbFailOnError

    db.CommitTrans ' <--- ここで即座にロック解放
    
    MsgBox "更新が正常に完了しました。", vbInformation
    
Exit_Sub:
    Set db = Nothing
    Exit Sub

Err_Handler:
    ' エラーが発生した場合は必ずロールバック
    ' dbがセットされており、かつトランザクション中であるかを確認する設計が理想
    If Not db Is Nothing Then
        On Error Resume Next ' Rollback自体が失敗する場合に備える
        db.Rollback
    End If
    
    MsgBox "致命的なエラーが発生しました。処理を中断し、データを保護しました。" & vbCrLf & _
           "Error No: " & Err.Number & vbCrLf & _
           "Description: " & Err.Description, vbCritical, "Transaction Error"
    Resume Exit_Sub
End Sub

' --- 補助関数(実際にはもっと複雑なロジックを想定) ---

Private Function CalculateNewQuantity(ByVal amount As Long) As Long
    ' 重たい計算のシミュレーション
    Dim i As Long
    For i = 1 To 1000000: Next i 
    CalculateNewQuantity = amount
End Function

Private Function IsValidPayload(ByRef p As UpdatePayload) As Boolean
    ' ビジネスルールに基づいたチェック
    IsValidPayload = (p.ProductID > 0)
End Function

このコードが優れている理由

1. ロック時間の極小化: `CalculateNewQuantity` という重い処理はトランザクションの外にある。`BeginTrans` から `CommitTrans` までの時間は、SQLの実行時間(ミリ秒単位)だけだ。
2. デッドロックの回避: 処理が高速であるため、他のユーザーとリソースが衝突する確率を数学的に最小化している。
3. 堅牢なエラーハンドリング: `Err_Handler` 内で確実に `Rollback` を行い、さらに `Rollback` 自体のエラーでプログラムがクラッシュするのを防いでいる。
4. 責務の分離: 「何を更新するか(Payload)」と「どう更新するか(Transaction)」が分かれているため、テストと保守が容易だ。

4. 外部データベース(SQL Server等)連携時の注意点

もし君が Access をフロントエンドとして、バックエンドに SQL Server を使っている場合、注意すべき点がある。

DAOは、ローカルの `.accdb` に対しては非常に強力だが、ODBC経由で外部DBを操作する場合、「サーバー側のロック挙動」が支配的になる。

  • トランザクションの隔離レベル: SQL Server側で「シリアライザブル(Serializable)」などの高い隔離レベルが設定されていると、DAOのトランザクションはより強力にレコードをロックし、デッドロックを誘発しやすくなる。
  • ADOの検討: 外部DBとの通信において、より高度なトランザクション制御(接続の明示的な管理や、サーバーサイドでのトランザクション処理)が必要な場合は、DAOではなく ADO (ActiveX Data Objects) を使用することを検討せよ。ADOは、ネットワーク越しでのデータ操作において、より現代的なアーキテクチャに適している。

まとめ:アーキテクトとしての心得

「動けばいい」という考え方は、プロフェッショナルではない。
「動いても、誰かの邪魔をしないか?」
「動いても、エラーの時にデータが壊れないか?」

この問いに答えられる設計こそが、真の業務自動化ツールだ。
トランザクションを「重い処理の塊」として使うな。「データの整合性を守るための、最小限の防壁」として扱え。

この設計パターンをマスターすれば、君の作るツールは、ユーザーが増えても、データが増えても、決して揺らぐことのない堅牢な基盤となるだろう。

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