データの整合性を守る最後の砦:DAOの「dbFailOnError」を使いこなす設計思想
開発現場において、もっとも恐ろしいのは「エラーが出ること」ではない。
「エラーが起きているのに、エラーが出ずに処理が完了してしまうこと」だ。
もし君が、数千件の在庫データを更新するクエリを走らせ、そのうちの1件がバリデーションエラーで更新に失敗したとする。しかし、プログラムは平然と「処理完了」を返し、ユーザーは笑顔で画面を閉じる。その裏で、帳簿と実在庫の不一致が発生しているのだとしたら――。
これは、単なるプログラミングミスではない。「データの整合性に対する設計思想の欠如」である。
今回は、Access VBAにおけるDAO(Data Access Objects)操作において、この「サイレント・エラー(静かなる失敗)」を阻止するための極めて強力な武器、`dbFailOnError` オプションについて、その真価と正しい設計パターンを伝授する。
1. なぜ、デフォルトの `Execute` は「危険」なのか
DAOの `Database.Execute` メソッドを使用する際、多くの開発者は以下のようなコードを書いてしまう。
' --- 典型的な、しかし危険なコード ---
CurrentDb.Execute "UPDATE T_Inventory SET Stock = Stock - 1 WHERE ProductID = 101"
この書き方では、もし `ProductID = 101` が存在しない、あるいは型変換エラーが発生したとしても、VBAはエラーを投げず、そのまま次の行へ進んでしまう。
DAOのデフォルト挙動は「可能な限り実行し、失敗しても無視する」という、ビジネスロジックとしては極めて不誠実なものなのだ。プロフェッショナルとして、このような「不完全な成功」を許してはならない。
2. `dbFailOnError`:Fail-Fast(早期失敗)の原則
ここで登場するのが `dbFailOnError` オプションである。これを使用することで、DAOエンジンに対して「一つでも失敗があれば、即座にエラーを発生させ、処理を中断せよ」という命令を下すことができる。
設計思想:Fail-Fast
ソフトウェア設計における重要な原則の一つに「Fail-Fast(早期失敗)」がある。問題が発生した際、それを隠蔽せず、即座に検知してシステムを停止させることで、被害(データの汚染)を最小限に食い止める考え方だ。
`dbFailOnError` は、VBAの世界における Fail-Fast を実現するためのスイッチなのだ。
3. 実践:堅牢な更新処理のプロダクションコード
では、実際にどのように実装すべきか。単にオプションを付けるだけでなく、エラーハンドリングと組み合わせた「実戦で使える」テンプレートを示そう。
''' <summary>
''' 在庫更新処理を実行する。
''' dbFailOnError を使用し、不完全な更新を許さない設計。
''' </summary>
Public Sub UpdateInventorySafe(ByVal targetProductID As Long, ByVal changeAmount As Integer)
Dim db As DAO.Database
Dim strSql As String
' エラーハンドリングの開始
On Error GoTo ErrorHandler
Set db = CurrentDb
' 更新SQLの構築
strSql = "UPDATE T_Inventory SET Stock = Stock + " & changeAmount & _
" WHERE ProductID = " & targetProductID
' 【重要】dbFailOnError を指定することで、更新に失敗した瞬間に ErrorHandler へジャンプする
' これにより、一部のレコードだけが更新されるといった「中途半端な状態」を検知できる
db.Execute strSql, dbFailOnError
Debug.Print "更新成功: " & targetProductID
ExitSub:
' オブジェクトの解放(メモリリーク防止の基本)
Set db = Nothing
Exit Sub
ErrorHandler:
' エラー内容のログ出力やユーザーへの通知
' ここでは実務を想定し、詳細なエラー情報を出力する
MsgBox "致命的なデータ更新エラーが発生しました。" & vbCrLf & _
"エラー番号: " & Err.Number & vbCrLf & _
"エラー内容: " & Err.Description, vbCritical, "システムエラー"
' プロダクション環境では、ここでログファイルへの書き出し等を行う
Resume ExitSub
End Sub
4. 【極限の知見】 `dbFailOnError` と「トランザクション」の決定的な違い
ここで、中級者が陥りやすい致命的な誤解を解いておこう。
「`dbFailOnError` を使っていれば、トランザクション(Commit/Rollback)は不要である」という考えは、完全に間違いだ。
この二つの役割は、全く異なる。
| 機能 | 役割 | 目的 |
|---|---|---|
| `dbFailOnError` | エラーの検知 | 「失敗したこと」をプログラムに知らせる(検知器) |
| トランザクション | 状態の復元 | 「失敗したとき」に、実行前の状態に戻す(復元装置) |
なぜ両方必要なのか?
例えば、「Aテーブルの在庫を減らす」→「Bテーブルの売上を増やす」という2ステップの処理を考えてほしい。
1. `dbFailOnError` を付けて、Aテーブルの更新に成功。
2. Bテーブルの更新でエラーが発生。
3. `dbFailOnError` によりエラーは検知できるが、Aテーブルの更新は既に確定(コミット)されている。
結果として、在庫だけ減って売上が上がっていないという、最悪のデータ不整合が発生する。
「検知(dbFailOnError)」して、「復元(Transaction)」する。
この両輪が揃って初めて、業務システムとしての信頼性が担保されるのだ。
理想的なアーキテクチャ例
Public Sub ComplexBusinessProcess()
Dim db As DAO.Database
Dim ws As DAO.Workspace
On Error GoTo ErrorHandler
Set ws = DBEngine.Workspaces(0)
Set db = CurrentDb
' --- トランザクション開始 ---
ws.BeginTrans
' 1. 在庫更新 (dbFailOnError で失敗を検知)
db.Execute "UPDATE T_Inventory SET Stock = Stock - 1 WHERE ID = 1", dbFailOnError
' 2. 売上記録 (dbFailOnError で失敗を検知)
db.Execute "INSERT INTO T_Sales (ProductID, Qty) VALUES (1, 1)", dbFailOnError
' 全ての工程が成功した場合のみ確定
ws.CommitTrans
MsgBox "全ての処理が正常に完了しました。"
ExitSub:
Set db = Nothing
Set ws = Nothing
Exit Sub
ErrorHandler:
' 失敗した場合、全ての操作を「開始前」の状態に巻き戻す
If Not ws Is Nothing Then ws.Rollback
MsgBox "プロセスが中断されました。全ての変更は取り消されました。" & vbCrLf & Err.Description, vbCritical
Resume ExitSub
End Sub
5. まとめ:アーキテクトからの提言
君が作成するツールが、単なる「動くコード」なのか、それとも「信頼されるシステム」なのか。その境界線は、こうした細部へのこだわりにある。
1. `Execute` を使うときは、常に `dbFailOnError` をセットで考えること。
2. 単一のSQLなら `dbFailOnError` で十分だが、複数の更新を伴うなら必ず `Transaction` を併用すること。
3. エラーを「隠す」のではなく、「検知して、適切に処理(あるいはロールバック)する」設計を徹底すること。
「たぶん大丈夫だろう」という甘い設計は、将来の自分、あるいは同僚を必ず苦しめることになる。堅牢なコードを書くことは、プロフェッショナルとしての誠実さそのものなのだ。
