Access VBAを掌握する極限の知見:トランザクションの限界を突破せよ!DAOとADOの擬似ネスト設計論
開発現場でよく見かける光景がある。「処理Aの中でトランザクションを張り、その中で呼び出す共通関数Bの中でもトランザクションを張る」。
一見、堅牢なエラーハンドリングをしているように見えるこのコードは、実務において「最悪のバグ」の温床となる。
結論から言おう。
DAO(Data Access Objects)もADO(ActiveX Data Objects)も、真の意味でのトランザクションのネスト(入れ子)をネイティブサポートしていない。
今回は、Access VBAにおけるトランザクションの限界の正体を解き明かし、複雑な業務ロジックで破綻しない「フラグ管理による擬似ネスト実装」を、プロダクション品質のコードとともに伝授する。
—
1. なぜ「トランザクションのネスト」で現場は崩壊するのか?
多くのプログラマが勘違いしているが、データベースのトランザクションは「階層構造」を持てない。多くのRDBMSやAccessの基盤であるJet/ACEエンジンにおいて、トランザクションは単一のフラットな状態(ONかOFFか)しか存在しない。
DAOの挙動:隠れたコミットの罠
DAOで `DBEngine.Workspaces(0).BeginTrans` を実行するとトランザクションが開始される。ここで重要なのは、「ネストしてもう一度 `BeginTrans` を呼ぶと、暗黙的に前のトランザクションがコミットされる(あるいはエラーになる)」 という仕様だ。
意図せず途中の状態で確定(コミット)されてしまい、ロールバックしたい場面でデータが部分的に書き残されるという、業務システムにおいて致命的なデータ破損を引き起こす。
ADOの挙動:プロバイダの限界
ADOの `Connection.BeginTrans` も同様だ。ADOの場合はネスト回数をカウントするプロバイダもあるが、AccessのローカルJet/ACEプロバイダ(Microsoft.ACE.OLEDB.12.0)では、トランザクションの入れ子は完全にサポートされていないか、予期せぬ例外を吐いて強制終了する。
> チーフアーキテクトの教訓
> 「共通関数だからとりあえずトランザクションを入れておこう」という甘えが、システム全体の原子性(Atomicity)を破壊する。トランザクションの制御権は、常にコールツリーの「最上位(エントリーポイント)」が握らなければならない。
—
2. 解決策:フラグ管理による「擬似ネスト」アーキテクチャ
では、トランザクションを含む共通処理を、別のトランザクション内の処理から呼び出したい場合はどうすればよいのか?
答えは 「外側がすでにトランザクション中なら、内側ではBeginTransをスキップし、エラー時の一括ロールバックは外側に委譲する」 という設計、すなわち状態管理フラグによる擬似ネストである。
今回は、実務で最も利用頻度が高い DAO をベースに、堅牢なトランザクション管理クラスの設計パターンを解説する。
—
3. 【実装例】プロダクションコード:DAOトランザクション管理モジュール
以下のコードは、呼び出し元がトランザクション中であればそれを維持し、未開始であれば新規にトランザクションを開始・管理する、実戦投入可能なデザインパターンだ。
標準モジュール:`mTransactionManager`
Option Compare Database
Option Explicit
‘ ==============================================================================
‘ módulo名: mTransactionManager
‘ 概要: DAOのトランザクションを安全に制御するラッパーモジュール
‘ ==============================================================================
‘ トランザクションのネスト深度を追跡する静的カウンタ
Private m_TransactionLevel As Long
”’
”’
Public Sub BeginTransactionSafe(ws As DAO.Workspace)
On Error GoTo ErrorHandler
If m_TransactionLevel = 0 ' 最初のトランザクション開始
ws.BeginTrans
Debug.Print “[Trans] トランザクションを開始しました。”
Else
Debug.Print “[Trans] 既存のトランザクションに相乗りします (深度: ” & m_TransactionLevel & “)”
End If
‘ 深度をインクリメント
m_TransactionLevel = m_TransactionLevel + 1
Exit Sub
ErrorHandler:
Err.Raise Err.Number, “BeginTransactionSafe”, “トランザクションの開始に失敗しました: ” & Err.Description
End Sub
”’
”’
Public Sub CommitTransactionSafe(ws As DAO.Workspace)
On Error GoTo ErrorHandler
If m_TransactionLevel <= 0 Then
Err.Raise 9999, "CommitTransactionSafe", "トランザクションが開始されていないのにコミットが呼ばれました。"
End If
' 深度をデクリメント
m_TransactionLevel = m_TransactionLevel - 1
If m_TransactionLevel = 0 Then
ws.CommitTrans dbForceOSFlush ' OSのディスクキャッシュまで確実に書き込み
Debug.Print "[Trans] トランザクションを正常にコミットしました。"
Else
Debug.Print "[Trans] 内層のコミット要求です。実体コミットは保留します (残り深度: " & m_TransactionLevel & ")"
End If
Exit Sub
ErrorHandler:
' 異常発生時は強制リセットの考慮が必要だが、基本は上位に委譲
Err.Raise Err.Number, "CommitTransactionSafe", "コミットに失敗しました: " & Err.Description
End Sub
'''
”’
Public Sub RollbackTransactionSafe(ws As DAO.Workspace)
On Error GoTo ErrorHandler
If m_TransactionLevel <= 0 Then Exit Sub
' 強制的に全層をロールバックするため、カウンタを無視して即座にRollback
ws.Rollback
m_TransactionLevel = 0
Debug.Print "[Trans] 【警告】トランザクションをロールバックしました。"
Exit Sub
ErrorHandler:
Debug.Print "[Trans] ロールバック処理中にエラーが発生しました: " & Err.Description
m_TransactionLevel = 0
End Sub
'''
”’
Public Function IsInTransaction() As Boolean
IsInTransaction = (m_TransactionLevel > 0)
End Function
—
4. 実業務ロジックでの利用イメージ
上記で作成した管理モジュールを、実際の業務データ登録処理(親テーブルと子テーブルの同時登録)に適用してみる。
”’
”’
Public Sub ProcessOrderEntry()
Dim ws As DAO.Workspace
Set ws = DBEngine.Workspaces(0)
‘ エラーハンドリングの要
On Error GoTo ErrorHandler
‘ 1. トランザクション開始
mTransactionManager.BeginTransactionSafe ws
‘ 2. 親データ(注文ヘッダ)の登録処理
Call InsertOrderHeader(ws, “ORD-2023-001”, “顧客A”)
‘ 3. 子データ(注文明細)の登録処理(ここでも内部でトランザクション制御を呼び出しても安全)
Call InsertOrderDetails(ws, “ORD-2023-001”)
‘ 4. すべて成功したらコミット
mTransactionManager.CommitTransactionSafe ws
MsgBox “データの登録が完了しました。”, vbInformation
Exit Sub
ErrorHandler:
‘ 5. 異常時は確実にロールバック
mTransactionManager.RollbackTransactionSafe ws
MsgBox “エラーが発生したため、処理を中断しました。” & vbCrLf & Err.Description, vbCritical
End Sub
”’
”’
Public Sub InsertOrderDetails(ws As DAO.Workspace, orderId As String)
On Error GoTo ErrorHandler
‘ この関数単体でテスト実行された場合も考慮してトランザクションをラップ
mTransactionManager.BeginTransactionSafe ws
‘ 明細データのSQL実行(ダミー)
CurrentDb.Execute “INSERT INTO T_OrderDetail (OrderId, ItemId, Qty) VALUES (‘” & orderId & “‘, ‘ITEM-01’, 10)”, dbFailOnError
mTransactionManager.CommitTransactionSafe ws
Exit Sub
ErrorHandler:
mTransactionManager.RollbackTransactionSafe ws
Err.Raise Err.Number, “InsertOrderDetails”, Err.Description
End Sub
—
5. 現場で絶対に守るべき運用ルール
1. `dbFailOnError` を忘れない
DAOの `CurrentDb.Execute` や `Recordset.Update` を使う際、エラー時に自動ロールバックをトリガーさせるために必ず `dbFailOnError` オプションを付与すること。これを怠ると、一部のレコードだけが挿入される「サイレント障害」を引き起こす。
2. トランザクション中のUI操作の排除
トランザクションのスコープ内で `MsgBox` を出したり、フォームの再描画を行ったりしてはならない。ロック保持時間が伸び、デッドロックやファイル破損(Access特有の `.laccdb` の競合)の原因になる。
3. ADOを使用する場合の注意点
ADO(`ADODB.Connection`)を使う場合も概念は全く同じだ。Connectionオブジェクトの `Errors` コレクションや独自の静的フラグ変数を用いて、ネスト構造をシミュレートする層を一枚挟むことで、コードの堅牢性は劇的に向上する。
—
総括
Access VBAにおいて、DAO/ADOの限界を嘆くのはプログラマの仕事ではない。限界を理解した上で、「どの層がトランザクションのライフサイクルを担保すべきか」をアーキテクチャとして設計することこそが、プロフェッショナルな業務自動化エンジニアの仕事である。
フラグ管理による擬似ネストをあなたのプロジェクトに導入し、冗長で堅牢な、絶対に壊れないバックエンドロジックを構築してほしい。
