【極限の堅牢設計】DAOの `dbFailOnError` を掌握せよ:サイレント・エラーを許さない、トランザクション制御の真髄
業務自動化ツールの開発において、最も恐ろしいバグは何だと考えるか?
構文エラーか? 無限ループか? いや、断じて違う。
「エラーが起きているのに、プログラムが正常終了した顔をして進んでしまうこと」
これこそが、実務レベルのシステムにおける最大の災厄だ。データの整合性が崩れ、帳簿が合わず、原因の特定すら困難な「サイレント・エラー」が、現場の信頼を根底から破壊する。
多くの開発者が、DAO(Data Access Objects)を使ってSQLを実行する際、単に `db.Execute strSQL` と書き、その結果を `True/False` で判定しようとする。だが、これは設計として極めて脆弱だ。
本記事では、Access VBAにおけるデータ操作の「防波堤」となる `dbFailOnError` オプションの真価と、それをトランザクション処理と組み合わせた、プロフェッショナルな堅牢設計の作法を伝授する。
1. なぜ、あなたの `Execute` は「嘘」をつくのか
DAOの `Database.Execute` メソッドは、デフォルトでは非常に「大人しい」挙動を示す。
通常、SQLの実行中に「入力規則違反」や「型不一致」「リレーションシップ違反(外部キー制約)」が発生しても、`Execute` メソッドは実行時エラーを発生させず、単に `False` を返すだけなのだ。
典型的な「素人」のコード
' 危険な例:エラーが発生しても気づかずに次へ進んでしまう
Dim strSQL As String
strSQL = "UPDATE T_在庫 SET 在庫数 = 在庫数 - 1 WHERE 商品ID = 'A001'"
' もし商品ID 'A001' が存在せず、更新件数が0件だったとしても、
' あるいは入力規則でエラーになっても、この行はエラーを出さずに通過する
db.Execute strSQL
' ここで次の処理(売上データの作成など)に進んでしまうと、
' 「在庫は減っていないのに、売上だけ上がっている」という致命的な不整合が生まれる
このように、戻り値の `Boolean` をチェックするだけの設計では、複雑なビジネスロジックの中ですべての例外を捕捉しきれる保証はない。我々エンジニアが求めているのは、「失敗したら即座に例外を投げて、処理を中断させる」という「Fail Fast(早く失敗せよ)」の思想である。
2. `dbFailOnError`:例外を強制する「隠れた切り札」
ここで登場するのが、`dbFailOnError` オプションだ。このフラグを `Execute` メソッドに渡すと、DAOの挙動は劇的に変わる。
「SQLの実行中に一つでも問題があれば、即座に実行時エラー(Runtime Error)を発生させる」
これこそが、堅牢なシステムに必要な挙動だ。エラーが発生すれば、VBAの標準的なエラーハンドリング(`On Error GoTo`)のルートへ強制的に遷移させることができる。
設計思想の転換
- Before: 実行結果を「戻り値」で確認する(受動的)
- After: 実行結果を「例外」として扱う(能動的)
3. トランザクションと組み合わせた「全か無か(Atomicity)」の実現
`dbFailOnError` の真の威力は、単体で使うときではなく、トランザクション処理(BeginTrans / CommitTrans / Rollback)と組み合わせたときに発揮される。
データベース操作において、複数のテーブルを更新する場合、それらは「すべて成功するか、すべて失敗するか(Atomicity:原子性)」のどちらかでなければならない。`dbFailOnError` がなければ、途中のSQLがサイレントに失敗しても、エラーハンドラが起動せず、中途半端な状態で `CommitTrans` が実行されてしまうのだ。
以下に、プロダクション環境でそのまま利用可能な、極限まで堅牢性を高めたコードパターンを示す。
【実践】堅牢なトランザクション・テンプレート
''
' 業務ロジック:在庫の減算と売上の記録をアトミックに実行する
' @description: dbFailOnErrorを使用し、エラー発生時に確実にロールバックを行う
''
Public Sub ExecuteOrderProcess(ByVal productID As String, ByVal quantity As Integer)
Dim db As DAO.Database
Dim strSQLStock As String
Dim strSQLSales As String
Set db = CurrentDb
' 1. トランザクションの開始
On Error GoTo ErrorHandler
DBEngine.BeginTrans
' SQLの構築
strSQLStock = "UPDATE T_在庫 SET 在庫数 = 在庫数 - " & quantity & _
" WHERE 商品ID = '" & productID & "' AND 在庫数 >= " & quantity
strSQLSales = "INSERT INTO T_売上 (商品ID, 数量, 日時) VALUES ('" & productID & "', " & quantity & ", Now())"
' 2. 在庫更新の実行
' dbFailOnErrorを付与することで、入力規則違反や条件不一致時にエラーを強制発生させる
db.Execute strSQLStock, dbFailOnError
' ※補足:SQL自体は成功しても「更新件数が0件」の場合、dbFailOnErrorだけではエラーにならない。
' 厳密なチェックが必要な場合は、db.RecordsAffected を確認するロジックを挟むこと。
If db.RecordsAffected = 0 Then
Err.Raise vbObjectError + 513, "ExecuteOrderProcess", "在庫不足または商品IDが存在しません。"
End If
' 3. 売上データの挿入
db.Execute strSQLSales, dbFailOnError
' 4. すべて成功したらコミット
DBEngine.CommitTrans
Debug.Print "Transaction Committed Successfully."
ExitProcedure:
' 後処理(オブジェクトの解放)
Set db = Nothing
Exit Sub
ErrorHandler:
' 5. エラーが発生した場合、即座にロールバック
' dbFailOnErrorのおかげで、ここへ確実に到達できる
DBEngine.Rollback
' エラー内容のログ出力やユーザー通知(実務ではログファイルへの書き出しを推奨)
MsgBox "致命的なエラーが発生したため、処理をロールバックしました。" & vbCrLf & _
"Error No: " & Err.Number & vbCrLf & _
"Description: " & Err.Description, vbCritical, "システムエラー"
Resume ExitProcedure
End Sub
4. アーキテクトの視点:運用上の注意点
この設計を導入するにあたり、以下の3点を忘れてはならない。
① `RecordsAffected` との使い分け
`dbFailOnError` は「SQLの構文や制約」に違反したときにエラーを投げる。しかし、「WHERE句の条件に合致せず、更新件数が0件だった」というケースは、SQLとしては正当なため、エラーにならない。
業務ロジック上、「更新件数が0件=異常」とみなすべき場合は、コード例に示したように `db.RecordsAffected` をチェックし、自ら `Err.Raise` を実行してエラーハンドラへ飛ばす設計が必要だ。
② 分割データベース(Frontend/Backend)におけるロック
トランザクションを開始すると、対象のレコードにロックがかかる。ネットワークが不安定な環境や、他のユーザーが大量の更新を行っている環境では、デッドロックやタイムアウトが発生しやすくなる。
`dbFailOnError` はエラーを確実に検知してくれるが、そのエラーが「データ不整合」なのか「単なるロック待ち(Timeout)」なのかを判別できるログ設計もセットで行うべきだ。
③ ADOとの混在を避ける
Accessにおいて、DAOはエンジンに最も近い、高速かつ高機能なオブジェクトである。ADO(ActiveX Data Objects)も強力だが、Access特有の機能(リレーションシップ制約や入力規則)を最も正確に、かつ高速にハンドリングできるのはDAOである。トランザクション制御を行うなら、一貫してDAOを使用することを強く推奨する。
結論
`dbFailOnError` は、単なるオプションではない。それは、「プログラムに誠実さを求めるための宣言」である。
「たぶん大丈夫だろう」という甘い設計は、必ずどこかで牙を剥く。エラーを隠蔽するのではなく、エラーを正しく扱い、失敗したときは潔くすべてをなかったことにする。この「失敗に対する備え」こそが、プロフェッショナルな業務アプリケーションと、単なるマクロを分かつ境界線なのだ。
