DAOトランザクションの深淵:デッドロックを回避する極限の排他制御設計
Access VBAを用いた大規模な業務アプリケーションにおいて、開発者が最も恐れるべきは「バグ」ではない。システムが静かに、しかし確実に沈黙する「デッドロック」と、ユーザーを拘束し続ける「ロック待ち」だ。
多くのエンジニアは `Workspace.BeginTrans` と `CommitTrans` の使い方を教わる。しかし、その背後で動いているJet/ACEエンジンがどのようにページレベルのロックを保持し、VBAのオブジェクトライフサイクルがどのように排他制御に干渉するかを理解している者は極めて少ない。
本稿では、DAO(Data Access Objects)を用いたトランザクション処理における排他制御のメカニズムを解剖し、高負荷環境下でも崩れない、堅牢なアーキテクチャ設計の真髄を伝授する。
1. DAOにおけるトランザクションの本質:Workspaceの支配権
まず、前提となるアーキテクチャを整理しておく。ADO(ActiveX Data Objects)がプロバイダーを介した抽象化された接続モデルであるのに対し、DAOはAccessのエンジン(ACE/Jet)に対して直接的に、かつ極めて密接に作用する。
DAOにおけるトランザクションの境界線は、`Database` オブジェクトではなく、その上位概念である `Workspace` オブジェクト が握っている。
Dim ws As DAO.Workspace
Set ws = DBEngine(0) ' デフォルトのワークスペースを取得
ws.BeginTrans ' ここからトランザクションの支配が始まる
ここで重要なのは、`BeginTrans` を呼び出した瞬間から、その `Workspace` インスタンスが管理するすべての操作が、コミットまたはロールバックが明示されるまで「未確定状態」としてエンジン内に留まるということだ。この「留まる」という行為が、他のユーザーに対するロック(Lock Wait)の正体である。
2. なぜ「ロック待ち」は発生し、デッドロックへ至るのか
デッドロックは、単なる「処理の遅延」ではない。複数のプロセスが互いに相手の解放を待ち続ける、論理的な膠着状態である。
原因A:トランザクション・スコープの肥大化
最も初歩的かつ致命的なミスは、トランザクションの範囲内に「ユーザー入力を待つ処理」や「重い外部API通信」を含めてしまうことだ。
- Bad Pattern: `BeginTrans` $\rightarrow$ [外部API呼び出し(5秒)] $\rightarrow$ `Update` $\rightarrow$ `CommitTrans`
この5秒間、対象のレコード(あるいはページ)はロックされ続け、他の全ユーザーの書き込みをブロックする。
原因B:オブジェクトの不完全な解放(メモリとリソースの不一致)
VBAのガベージコレクションは、C++のような厳密な決定論的破棄(Deterministic Destruction)ではない。エラーハンドリングが不適切で、`Recordset` や `Database` オブジェクトがメモリ上に残存した場合、それらが保持しているロックが、コード上のスコープを抜けてもエンジン内に残り続けることがある。これが「プロセスは生きているが、データがロックされたまま動かない」という怪現象の正体だ。
原因C:アクセス順序の不一致(デッドロックの王道)
- ユーザーA:テーブル1を更新 $\rightarrow$ テーブル2を更新
- ユーザーB:テーブル2を更新 $\rightarrow$ テーブル1を更新
この順序の不整合が、トランザクションの最中に発生した瞬間、両者は永久に互いを待ち続ける。
3. 極限の設計パターン:堅牢なトランザクションの実装
プロフェッショナルな設計においては、「アトミック性(不可分性)」と「最短経路の確保」を徹底する。以下に、私が推奨する標準的な実装パターンを示す。
実装コード例:堅牢なデータ更新ルーチン
''' <summary>
''' 高信頼性データ更新プロシージャ
''' </summary>
Public Sub ExecuteAtomicUpdate(ByVal targetID As Long, ByVal newValue As String)
Dim ws As DAO.Workspace
Dim db As DAO.Database
Dim rs As DAO.Recordset
Dim errNum As Long
' 1. Workspaceの取得(トランザクション管理の起点)
Set ws = DBEngine(0)
On Error GoTo ErrorHandler
' 2. トランザクション開始
' ※ここより前に、ユーザー入力や重い計算処理を完了させておくこと
ws.BeginTrans
Set db = CurrentDb
' 3. レコードセットのオープン
' LockTypeを更新可能に、Optionsをダイナミックに設定
Set rs = db.OpenRecordset("SELECT * FROM T_Master WHERE ID = " & targetID, _
dbOpenDynaset, dbSeeChanges)
If rs.EOF Then
Err.Raise vbObjectError + 513, "ExecuteAtomicUpdate", "対象レコードが存在しません。"
End If
' 4. データ更新操作(極力短時間で行う)
rs.Edit
rs!TargetField = newValue
rs.Update
' 5. コミット(成功時)
ws.CommitTrans
Debug.Print "Transaction Committed Successfully."
ExitRoutine:
' 6. 明示的なリソース解放(これが極めて重要)
' エラー時でも必ずオブジェクトをNothingにすることで、
' エンジンにロックの解放を通知する。
On Error Resume Next
If Not rs Is Nothing Then
rs.Close
Set rs = Nothing
End If
If Not db Is Nothing Then Set db = Nothing
' WorkspaceはBeginTransを呼んだものと同じものを使うが、
' 参照を外すことでライフサイクルを制御する
Set ws = Nothing
Exit Sub
ErrorHandler:
errNum = Err.Number
Debug.Print "Error Occurred: " & errNum & " - " & Err.Description
' 7. ロールバック(失敗時)
' 失敗した場合は即座にロールバックし、ロックを強制解除する
If Not ws Is Nothing Then
ws.Rollback
Debug.Print "Transaction Rolled Back."
End If
' エラー情報を上位へ伝播
Resume ExitRoutine
End Sub
4. アーキテクトの知恵:パフォーマンスと整合性のトレードオフ
さらに上流の設計を行う際、以下の3点を意識せよ。
I. インデックスの最適化によるページロックの回避
DAOのロックは、レコード単位(Record-level)で行われる設定であっても、実際の物理構造(B-Tree)においてはページ単位(Page-level)の影響を受ける。検索条件となるフィールドに適切なインデックスが貼られていないと、エンジンはフルスキャンを試み、広範囲のページをロックしてしまう。「インデックス不足は、ロック範囲の拡大を意味する」ことを忘れるな。
II. Windows APIによるプロセスの監視(高度な保守)
レガシーな環境において、どうしてもロックが解けない「ゾンビ・トランザクション」が発生した場合、VBA内部からの解決は困難だ。その際は、Windows APIを用いて、対象の `.ldb` または `.laccdb` ファイルを保持しているプロセスを特定し、管理者に通知する仕組みを検討せよ。
III. ADOとの使い分け
もし、システム間連携(SQL Serverへの移行を見据えた設計など)が前提であれば、トランザクション制御をADO(またはOLEDBプロバイダー)に委ねる方が、分散トランザクション(MSDTC)の管理において有利に働く場合がある。DAOはあくまで「Access内での高速・密結合な操作」に特化させるべきだ。
結論
DAOのトランザクション設計において、もっとも重要なのはコードの美しさではない。「いかに早く、いかに確実に、ロックを解放するか」という一点に尽きる。
`BeginTrans` を呼ぶ前にすべての準備を整え、`Commit/Rollback` を呼んだ直後にすべてのオブジェクトを `Nothing` で葬る。この規律こそが、大規模システムを支えるエンジニアの矜持である。
