DAO.Database.Executeの「dbFailOnError」:静かなるデータ破壊を防ぐための極限の防壁
Access VBAにおけるデータ操作において、`DoCmd.RunSQL` や無防備な `CurrentDb.Execute` を安易に使う者は、プログラミングというよりも「ロシアンルーレット」を楽しんでいるに近い。
データベースの操作において最も恐れるべきは、エラーが発生することそのものではない。「エラーが発生したにもかかわらず、システムがそれを検知できず、中途半端なデータがコミットされること(サイレント・フェイル)」これに尽きる。
今回は、DAOの `Execute` メソッドにおける `dbFailOnError` オプションの本質と、レガシー環境からモダンな連携までを見据えたトランザクション制御の極限の知見を解説する。
—
1. なぜ `CurrentDb.Execute` はデフォルトで「嘘をつく」のか
Access VBAでSQLを実行する際、多くの初学者は次のようなコードを書く。
‘ 悪夢への第一歩
CurrentDb.Execute “UPDATE T_Stock SET Quantity = Quantity – 1 WHERE ItemID = ‘A001′”
このコードの何が問題か。もし `ItemID = ‘A001’` が存在しなかったり、データ型のエラーや値のオーバーフローが発生したりした場合でも、`CurrentDb.Execute` はエラーを投げずに素知らぬ顔で処理を続行することがある(※一括処理のバッチ実行時など、条件エンティティの評価によってはエラーが握りつぶされる挙動を示す)。
さらに悪質なのは、複数レコードを更新するトランザクション的なSQL(例:一括ステータス更新)において、前半の10件は成功し、11件目で制約違反(重複キーやNull違反など)が発生した場合、「前半の10件は更新されたまま、後半だけが失敗する」という最悪のデータ不整合(ダーティ・リード/部分更新)を引き起こす点だ。
この挙動を断ち切り、アトマシティ(原子性:すべて成功するか、すべて失敗するか)を担保するのが `dbFailOnError` である。
—
2. `dbFailOnError` の真価とエラーハンドリングの鉄則
`dbFailOnError` を付与することで、SQLの実行中にわずかでもエラー要因が発生した場合、DAOエンジンは即座に例外(Run-time error)を発生させ、そのトランザクションをロールバックする。
シニアエンジニアであれば、このオプションを単体で使うのではなく、必ず `On Error` 構文と組み合わせ、DAOの固有エラー(`DBEngine.Errors`)を捕捉する構造を構築しなければならない。
以下に、実務でそのまま使える堅牢な更新処理のテンプレートを示す。
Public Sub ExecuteCriticalUpdate()
Dim db As DAO.Database
Dim strSQL As String
‘ CurrentDbの乱用を避け、変数に参照を保持する(パフォーマンスとメモリ管理の基本)
Set db = CurrentDb()
‘ トランザクションの明示的開始
db.BeginTrans
On Error GoTo ErrorHandler
strSQL = “UPDATE T_OrderDetails SET Status = 2 WHERE ProcessDate < #2023-01-01#" ' 【極限の知見】dbFailOnErrorを指定し、失敗を確実に例外化する db.Execute strSQL, dbFailOnError ' 全処理が成功した場合のみコミット db.CommitTrans MsgBox "更新処理が正常に完了しました。", vbInformation GoTo CleanUp ErrorHandler: ' 発生したエラーをロールバック db.Rollback ' DAO固有のエラー詳細をログやイミディエイトウィンドゥに出力 Dim errLoop As DAO.Error For Each errLoop In DBEngine.Errors Debug.Print "Error #: " & errLoop.Number & " - " & errLoop.Description Next errLoop MsgBox "データの更新に失敗しました。処理をロールバックします。(Error: " & Err.Description & ")", vbCritical CleanUp: ' オブジェクトの明示的解放(メモリリークの防止) Set db = Nothing End Sub ---
3. チーフアーキテクトが教える:オブジェクトのライフサイクルとパフォーマンス最適化
Access VBAの現場で最も頻繁に見るアンチパターンが、コードの至るところでの `CurrentDb.Execute` の乱用だ。
CurrentDb() の正体を知る
`CurrentDb()` 関数は、呼び出されるたびに新しい `Database` オブジェクトをメモリ上に生成し、内部的なシステムテーブルとのセッションを確立する。これをループ内で何回も呼び出すと、Accessの限られたメモリ空間とリソースを圧迫し、パフォーマンスが著しく低下するだけでなく、最悪の場合はメモリリークや「リソース不足」エラーを引き起こす。
- 対策: データベースへの参照はプロシージャの最初に一度だけ変数(`Dim db As DAO.Database`)に取得し、使い回せ。処理の終了時には必ず `Set db = Nothing` で解放する。
トランザクションのスコープ
`db.BeginTrans` から `db.CommitTrans`(または `Rollback`)までのスコープは、極力短く保つべきである。この間にユーザーへのプロンプト(`MsgBox`)を挟むような愚行を犯してはならない。ロック競合を引き起こし、マルチユーザー環境のAccessシステムを完全に崩壊させる原因となる。
—
4. システム間連携における堅牢性:VBAと外部DBの狭間で
レガシーなAccessシステムでは、基幹系データベース(SQL ServerやOracleなど)のビューやリンクトテーブルに対して、VBAからDAO経由で更新をかけるケースが多々存在する。
ネットワークの瞬断や、ODBCドライバのタイムアウト、リモート側でのロック競合が発生するこうした環境において、`dbFailOnError` なしでのデータ更新は自殺行為に等しい。リモート側で途中まで処理が走った挙句に接続が切れた場合、ローカルのVBA側からは「何が成功して何が失敗したか」を追跡できなくなるからだ。
リモート連携や大量レコードのバルク更新を行う場合は、必ず `dbFailOnError` とトランザクションをセットで実装し、失敗時にはODBCのエラーコードまでハンドリングできるアーキテクチャを設計しなければならない。
—
総括
`DAO.Database.Execute` の第2引数に `dbFailOnError` を記述することは、プログラマの「お作法」ではない。それは「データ汚染を絶対に許さない」というエンジニアのエンジニアリングに対する誓約である。
動けばいいという甘えたコードを捨て、オブジェクトのライフサイクルを制御し、あらゆる異常系を想定した堅牢なコードベースを構築すること。それこそが、レガシーシステムを生き延びさせ、真に信頼されるアーキテクチャを築く唯一の道である。
