DAOの「dbFailOnError」を掌握せよ:データ整合性を死守するアーキテクトの思考法
多くの開発者が、Access VBAを単なる「小規模なツール作成言語」と侮っている。しかし、その認識こそが、大規模なレガシーシステムを運用する上で致命的なバグやデータ不整合を招く元凶だ。
現場でシニアエンジニアが直面するのは、プログラムが止まるエラーではない。「エラーが出ずに、一部のデータだけが書き換わっていない」という、静かなる崩壊である。
本稿では、DAO(Data Access Objects)における更新処理の安全性、とりわけ `dbFailOnError` オプションの真の価値と、それを支えるオブジェクトのライフサイクル管理について、極限の視点から解説する。
1. 「静かなる失敗」という最悪のシナリオ
Access VBAでSQL文を実行する際、多くの人間は以下のようなコードを書く。
' 典型的な、しかし極めて危険なコード
CurrentDb.Execute "UPDATE T_Inventory SET Stock = Stock - 1 WHERE ItemID = 101"
このコードには、致命的な欠陥がある。もし `ItemID = 101` のレコードが、他のプロセスによってロックされていたり、入力規則(Validation Rule)に抵触したりした場合、この `Execute` メソッドは何も言わずに終了する。
エラーは発生しない。実行結果も `True` を返す。しかし、在庫は減っていない。これが「静かなる失敗」だ。データ整合性が失われた瞬間、システムとしての信頼性は死ぬ。
2. `dbFailOnError`:エンジンに「妥協」を許さない命令
ここで登場するのが、DAOの真骨頂とも言える `dbFailOnError` オプションである。
' プロフェッショナルのための最小構成
CurrentDb.Execute "UPDATE T_Inventory SET Stock = Stock - 1 WHERE ItemID = 101", dbFailOnError
このオプションを付与することで、Jet/ACEエンジンに対して「一つでも更新に失敗したら、その処理全体をエラーとして報告せよ」と命じることができる。これにより、実行時エラーとして例外がスローされ、VBAの標準的なエラーハンドリング(`On Error GoTo`)のフローに乗せることが可能になる。
なぜこれだけで「トランザクション」に近い動きをするのか?
`dbFailOnError` を指定した単一の `Execute` 文は、そのSQL文がアトミック(不可分)に実行されることを保証する。SQL文が複数のレコードを更新対象とする場合、その一連の更新のいずれかが失敗すれば、エンジンはそれまでの変更をロールバックし、エラーを通知する。
ただし、注意せよ。これは「単一のSQL文」に対するアトミシティだ。複数の `Execute` 文にまたがるビジネスロジックの整合性を保つには、後述する明示的なトランザクション制御が必要となる。
3. トランザクションの階層構造:単一実行 vs 複数実行
アーキテクトは、エラー検知のレベルを使い分ける。
| レベル | 手法 | 用途 | 整合性の範囲 |
|---|---|---|---|
| Level 1 | `dbFailOnError` | 単一のSQL実行 | そのSQL文内のみ |
| Level 2 | `BeginTrans / CommitTrans` | 複数の更新ステップ | 業務プロセス全体 |
複数のテーブルを跨ぐ更新(例:受注ヘッダの更新 + 受注明細の追加 + 在庫の減算)を行う場合、`dbFailOnError` だけでは不十分だ。一連の処理のどこかで失敗した場合、それ以前の成功した `Execute` も取り消さなければならない。
4. オブジェクトのライフサイクル:メモリとロックの管理
シニアエンジニアとして、コードの堅牢性と同時に、システムの持続可能性(パフォーマンスとリソース管理)に責任を持たなければならない。VBAにおけるオブジェクトの扱い、特にDAOオブジェクトの解放は、Windows APIレベルでのメモリ管理に近い厳格さが求められる。
メモリリークとファイルロックの回避
DAOの `Database` オブジェクトや `Recordset` オブジェクトを適切に解放しない場合、メモリ上にゴミが溜まるだけでなく、`.accdb` ファイルに対して「不当なロック」が残り続ける。これは、共有ネットワーク環境で複数のクライアントがアクセスするシステムにおいて、致命的な競合を引き起こす。
' 悪い例:スコープが不明確で、解放が曖昧
Sub BadPractice()
Dim db As DAO.Database
Set db = CurrentDb
db.Execute "...", dbFailOnError
' db を Nothing にしていない。エラー発生時に解放されないリスクがある。
End Sub
黄金のパターン:堅牢な更新プロシージャ
以下に、私が設計において推奨する、エラーハンドリング、トランザクション、およびオブジェクト解放を網羅したテンプレートを示す。
''' <summary>
''' 在庫更新を含む一連の業務処理をアトミックに実行する
''' </summary>
Public Sub ExecuteOrderProcess(ByVal orderID As Long, ByVal itemID As Long, ByVal qty As Integer)
Dim db As DAO.Database
Dim ws As DAO.Workspace
' 1. オブジェクトの初期化
Set ws = DBEngine(0) ' デフォルトのワークスペースを取得
Set db = CurrentDb
On Error GoTo ErrorHandler
' 2. 明示的なトランザクションの開始
' dbFailOnError と組み合わせることで、論理的整合性を極限まで高める
ws.BeginTrans
' A. 受注ヘッダの更新
db.Execute "UPDATE T_Orders SET Status = 'Processed' WHERE OrderID = " & orderID, dbFailOnError
' B. 在庫の減算(ここで在庫不足のバリデーションエラーが起きると、Aもロールバックされる)
db.Execute "UPDATE T_Inventory SET Stock = Stock - " & qty & " WHERE ItemID = " & itemID, dbFailOnError
' 3. 全ての処理が成功した場合のみコミット
ws.CommitTrans
Debug.Print "Transaction Committed Successfully."
ExitProcedure:
' 4. オブジェクトの明示的解放(ここがアーキテクトの規律)
' エラーの有無に関わらず、必ず実行されるセクション
On Error Resume Next ' 解放時のエラーでエラーハンドラを混乱させない
If Not db Is Nothing Then Set db = Nothing
If Not ws Is Nothing Then Set ws = Nothing
Exit Sub
ErrorHandler:
' 5. エラー発生時のロールバック
If Not ws Is Nothing Then
ws.Rollback
Debug.Print "Transaction Rolled Back due to error."
End If
' 詳細なエラーログの記録(実務ではファイルやテーブルへ出力)
MsgBox "Critical Error: " & Err.Number & " - " & Err.Description, vbCritical, "System Error"
Resume ExitProcedure
End Sub
5. 結論:エンジニアとしての規律
`dbFailOnError` は、単なるオプションではない。それは、「データの不整合を許容しない」という設計者の意志の表明である。
シニアエンジニアは、コードが「動くこと」をゴールとはしない。「予期せぬ事態において、いかに美しく、いかに安全に失敗するか」を設計の主眼に置く。
- `dbFailOnError` をデフォルトの思考とする。
- 複数ステップの更新には必ず `Workspace` によるトランザクションを。
- オブジェクトのライフサイクルを完全に制御し、リソースの枯渇を防ぐ。
この規律こそが、レガシーな環境であっても、数十年動き続ける堅牢なシステムを構築するための唯一の道である。
