DoCmd.RunSQLとCurrentDb.Executeの決定的な差異:トランザクション制御の極限領域
Access VBAによるエンタープライズシステムの開発において、データベースへの直接的なSQL発行は日常茶飯事である。しかし、多くのプログラマは「動けばいい」という安易な動機から、`DoCmd.RunSQL` と `CurrentDb.Execute` を気分次第で使い分けている。
シニアエンジニアであれば、この二者の選択がシステムの堅牢性、パフォーマンス、そしてトランザクションの成否を分ける致命的な分岐点であることを知っているはずだ。
本稿では、Accessオブジェクトモデルの深層に踏み込み、警告抑制とトランザクション制御、そしてメモリ管理の観点から、この二つのSQL実行手段の優劣を徹底的に解剖する。
—
1. 内部アーキテクチャの根本的な違い
`DoCmd.RunSQL`:UI層の遺物
`DoCmd` オブジェクトは、本質的にユーザーインターフェース(UI)の操作をエミュレートするマクロの延長である。
- パフォーマンス: UIスレッドを介するためオーバーヘッドが大きい。
- 環境依存: `SetWarnings` による警告の抑制がグローバルな設定に依存するため、マルチスレッドや非同期処理の文脈において極めて脆弱。
- エラーハンドリング: 実行時エラーが発生した際の挙動が不安定であり、DAOのエラーオブジェクト (`Err`) へ正確な情報を返さない場合がある。
`CurrentDb.Execute`:データアクセス層の牙城
一方、`CurrentDb.Execute` はDAO(Data Access Objects)のデータベースエンジンを直接たたく。
- パフォーマンス: UIをバイパスするため、圧倒的に高速。
- 独立性: 警告メッセージはオプション引数(`dbFailOnError`)で完全に制御可能であり、グローバルステートを汚染しない。
- エラーハンドリング: トランザクションとの統合性が高く、エンジンのネイティブなエラーを正確にキャッチできる。
—
2. トランザクション制御のパラダイム:なぜ `DoCmd` では不十分なのか
業務システムにおける一括処理(例:親レコードの登録と、それに紐づく子レコード群の大量INSERT)において、途中でエラーが発生した場合は「すべてロールバックする」ことが絶対要件となる。
ここで `DoCmd.RunSQL` を用いた場合、トランザクションの境界を設定しても、DAOのトランザクションログとUIの処理が競合し、意図せぬ部分でコミットされてしまう現象(幽霊コミット)が発生しうる。
確実なトランザクション制御を行うためには、`Workspace` オブジェクトの明示的なトランザクション制御と、`CurrentDb.Execute` の `dbFailOnError` オプションを組み合わせる必要がある。
—
3. 実装コード:極限まで最適化されたトランザクションパターン
以下のコードは、数万件規模のレコード処理を想定し、メモリのリークを防ぐためのオブジェクトの明示的解放、警告の完全な局所制御、そして確実なロールバックを実装したプロダクションレベルのサンプルである。
Option Compare Database
Option Explicit
‘ ==============================================================================
‘ 処理名: ExecuteBatchWithTransaction
‘ 概要: 堅牢なトランザクション制御を用いた一括データ処理の模範実装
‘ 備考: DoCmdは一切使用せず、DAOのネイティブ機能のみで完結させる
‘ ==============================================================================
Public Sub ExecuteBatchWithTransaction()
Dim ws As DAO.Workspace
Dim db As DAO.Database
Dim lngRecordsAffected As Long
‘ エラーハンドリングの準備
On Error GoTo ErrorHandler
‘ 1. デフォルトワークスペースの取得
Set ws = DBEngine.Workspaces(0)
‘ 2. トランザクションの開始
ws.BeginTrans
‘ 3. カレントデータベースの参照をローカル変数に保持(※重要:毎回CurrentDbを呼ばない)
Set db = CurrentDb
‘ — 処理フェーズ 1: ログまたは一時テーブルの初期化 —
‘ dbFailOnErrorを指定することで、エラー時に即座にトラップ可能にする
db.Execute “DELETE FROM T_TempWorkingTable;”, dbFailOnError
‘ — 処理フェーズ 2: メインのデータ挿入(例:重い集計クエリの結果を突合) —
db.Execute _
“INSERT INTO T_TempWorkingTable (TargetID, ProcessDate) ” & _
“SELECT ID, Date() FROM M_Master WHERE Status = 1;”, _
dbFailOnError
‘ 影響を受けたレコード数の確認が必要な場合は RecordsAffected プロパティを参照
lngRecordsAffected = db.RecordsAffected
Debug.Print “処理レコード数: ” & lngRecordsAffected
‘ — 処理フェーズ 3: 関連テーブルの更新 —
db.Execute _
“UPDATE T_TargetTable T ” & _
“INNER JOIN T_TempWorkingTable W ON T.ID = W.TargetID ” & _
“SET T.Flag = -1;”, _
dbFailOnError
‘ 4. 全ての処理が成功した場合のみコミット
ws.CommitTrans
MsgBox “トランザクションが正常に完了しました。”, vbInformation, “成功”
GoTo Finally
ErrorHandler:
‘ 5. 異常系:ロールバックの実行
If Not ws Is Nothing Then
ws.Rollback
End If
‘ 致命的なエラーの伝播
MsgBox “致命的なエラーが発生したため、変更を破棄しました。” & vbCrLf & _
“Error No: ” & Err.Number & vbCrLf & _
“Description: ” & Err.Description, vbCritical, “ロールバック実行”
Finally:
‘ 6. メモリの明示的解放(オブジェクト参照の破棄)
‘ ※Access VBAではローカル変数であっても確実にNothingを代入し、
‘ COMコンポーネントの参照カウンタを即座にデクリメントすることがメモリ最適化の極意。
Set db = Nothing
Set ws = Nothing
Exit Sub
End Sub
—
4. シニアエンジニアが知るべき「裏の知見」とメモリ最適化
上記のコードにおいて、なぜ `CurrentDb` をプロパティのまま何度も呼び出さず、変数 `db` に一度だけ代入しているのか?
ここがAccess VBAの挙動を熟知しているかどうかのリトマス試験紙である。
`CurrentDb` の正体
`CurrentDb` メソッドは、呼び出されるたびに内部で新しい `Database` オブジェクトのインスタンスを生成し、カレントデータベースへの参照を返している。
ループ内や複数回にわたって `CurrentDb.Execute` を直叩きすると、以下の弊害が生じる。
1. メモリリークの温床: 内部で生成されたオブジェクトがガベージコレクションのタイミングまで残り続け、Accessのメモリフットプリントが肥大化する。
2. パフォーマンスの劣化: 毎回COMオブジェクトの生成・破棄のオーバーヘッドが発生する。
したがって、モジュール内でデータベース操作を行う際は、最初に `Set db = CurrentDb` としてローカル変数にキャッシュし、処理終了時に必ず `Set db = Nothing` で解放するのが、数十年耐えうるレガシーシステム保守の鉄則である。
—
5. 結論
警告の抑制を安易に `DoCmd.SetWarnings False` で行い、エラー処理を怠るコードは、もはやプロフェッショナルの書くものではない。
- UI操作を伴うマクロ移行期を除き、`DoCmd.RunSQL` は封印せよ。
- データ操作のすべては `CurrentDb.Execute` と `dbFailOnError` で行え。
- トランザクションは `Workspace` で制御し、例外時は必ず `Rollback` を通せ。
- オブジェクトは使い捨てず、明示的に変数へキャッシュして即座に解放せよ。
この原則を死守するだけで、Accessアプリ特有の「突然のデータベース破損」や「原因不明のメモリ肥大化エラー」は劇的に激減する。技術の深淵を理解した者だけが、安定した堅牢なシステムを構築できるのだ。
