【中級】DAO.QueryDefでパラメータクエリをVBAから安全に実行する:SQLインジェクションの排除と極限のメモリ最適化
レガシーシステムの最前線に立ち続けるエンジニアであれば、VBAのコード内に散らばる「文字列結合によるSQL構築」がどれほど脆弱で、かつパフォーマンス上の爆弾を抱えているか身をもって知っているはずだ。
`”SELECT FROM T_Sales WHERE CustomerID = ” & Me.txtID`
このようなコードを書く人間は、明日からインフラの保守に回すべきだ。SQLインジェクションの危険性はもちろんのこと、Accessのクエリプロセッサ(Jet/ACEエンジン)に毎回新しいSQL文をパースさせ、実行計画を再構築させるコストは、システムが大規模化するにつれて確実にデータベースを窒息させる。
今回は、DAO(Data Access Objects)の `QueryDef` オブジェクトを駆使し、パラメータクエリを安全かつ極限まで最適化された状態で実行する手法を、アーキテクトの視点から解説する。
—
1. なぜ `CurrentDb.Execute` の文字列結合は悪なのか
多くの開発者は、アクションクエリを実行する際に安易に以下のようなコードを書く。
‘ 【アンチパターン】絶対にやってはならない実装
Dim sql As String
sql = “UPDATE T_Data SET Status = 2 WHERE Category = ‘” & Me.txtCategory & “‘”
CurrentDb.Execute sql, dbFailOnError
このアプローチには致命的な欠陥が3つある。
1. セキュリティの欠如: 入力値にシングルクォートが含まれている場合の構文エラー、あるいは悪意ある入力によるSQLインジェクションの温床となる。
2. パフォーマンスの劣化(プランキャッシュの破棄): 文字列が毎回異なるため、ACEエンジンはクエリの実行計画(Execution Plan)をキャッシュできず、パース処理のオーバーヘッドが常に見えざるコストとして発生する。
3. 型変換の脆弱性: 日付型や数値型のフォーマット(特にロケール依存の問題)において、意図しない解釈をされるリスクがある。
これらを根絶するのが `DAO.QueryDef` と明示的なパラメータバインディングである。あらかじめコンパイルされたクエリのひな形に対し、型安全なパラメータを流し込む。これがプロフェッショナルのアプローチだ。
—
2. アーキテクトが実践する `QueryDef` によるパラメータクエリの実装
あらかじめAccessのクエリウィンドウでパラメータクエリ(例:`qryUpdateStatus`)を作成しておくか、VBAのコード内で一時的なQueryDefを生成して実行する。ここでは、保守性とパフォーマンスの観点からベストとされる「あらかじめ定義されたクエリDefsの利用」コードを示す。
事前準備(Accessのクエリデザイナでの定義)
クエリ名:`q_UpdateStock`
SQL文:
PARAMETERS p_Qty Long, p_ItemID Text(50);
UPDATE T_Inventory SET Stock = Stock – [p_Qty] WHERE ItemID = [p_ItemID];
VBAでの安全な実行コード
‘ =========================================================================
‘ 処理名: パラメータクエリを使用した安全かつ高速な在庫更新
‘ 備考: オブジェクトのライフサイクルを厳密に管理し、メモリリークを防ぐ
‘ =========================================================================
Public Sub ExecuteStockUpdate(ByVal targetItemID As String, ByVal deductQty As Long)
Dim db As DAO.Database
Dim qdf As DAO.QueryDef
‘ 1. 現在のデータベースインスタンスを取得
‘ ※ CurrentDbを直接叩き続けるのはCOMのオーバヘッドを生むため変数に保持する
Set db = CurrentDb
On Error GoTo ErrorHandler
‘ 2. QueryDefオブジェクトの参照を取得
Set qdf = db.QueryDefs(“q_UpdateStock”)
‘ 3. パラメータに型安全な値をバインド
qdf.Parameters(“p_Qty”) = deductQty
qdf.Parameters(“p_ItemID”) = targetItemID
‘ 4. トランザクションの開始(必要に応じて)
db.BeginTrans
‘ 5. クエリの実行 (dbFailOnErrorでエラー時はロールバック可能にする)
qdf.Execute dbFailOnError
‘ トランザクション確定
db.CommitTrans
Debug.Print “正常終了: ItemID ” & targetItemID & ” の在庫を ” & deductQty & ” 減算しました。”
CleanUp:
‘ 6. オブジェクトの明示的解放(メモリ最適化の極意)
‘ ※ VBAのガベージコレクタを信用せず、スコープを抜ける前に確実に破棄する
If Not qdf Is Nothing Then
qdf.Close
Set qdf = Nothing
End If
Set db = Nothing
Exit Sub
ErrorHandler:
‘ エラー発生時はロールバック
db.Rollback
MsgBox “エラー番号: ” & Err.Number & vbCrLf & Err.Description, vbCritical, “致命的エラー”
Resume CleanUp
End Sub
—
3. メモリ最適化とオブジェクトライフサイクルの真実
シニアエンジニアであれば、Access VBAにおける「暗黙のインスタンス化」や「COMオブジェクトの解放漏れ」が、デスクトップアプリケーションの寿命をいかに縮めるかを知っているはずだ。
`CurrentDb` の罠
`CurrentDb` は呼び出すたびに新しい `Database` オブジェクトのインスタンスをメモリ上に生成する。これをループ内で `CurrentDb.Execute` のように直接叩くと、内部のCOM参照カウンタが狂い、リソースリークや最悪の場合は `.accdb` ファイルの破損(Corruption)を引き起こす。
> 鉄則: `CurrentDb` は必ず変数に一度だけ代入し、そのスコープ内でのみ使い回せ。処理が終了したら `Set db = Nothing` で確実に解放する。
`QueryDef.Close` の重要性
`QueryDef` オブジェクトも同様だ。特に永続的なクエリ(Accessの「クエリ」タブに保存されているもの)であっても、コード内で `QueryDefs(“…”)` として取得して操作した後は、`qdf.Close` によって内部バッファとハンドルを明示的に解放すべきである。
—
4. 動的クエリ(一時QueryDef)の極限活用
アプリケーションの要件として、あらかじめ固定のクエリとして定義できない、動的な条件構築が必要な場合もあるだろう。その場合でも、文字列結合のまま `CurrentDb.Execute` するのではなく、「一時QueryDef(Temporary QueryDef)」をコード上で動的に生成し、パラメータを渡して即座に破棄する手法をとるべきだ。
Public Sub ExecuteDynamicParamQuery(ByVal sqlCriteria As String, ByVal paramValue As Variant)
Dim db As DAO.Database
Dim qdf As DAO.QueryDef
Dim dynamicSQL As String
Set db = CurrentDb
‘ 基本となるSQL構造(プレースホルダーとしてのパラメータ名を設定)
dynamicSQL = “PARAMETERS p_Value Long; SELECT FROM T_Log WHERE LogLevel >= [p_Value];”
On Error GoTo ErrorHandler
‘ クエリの名前を空文字(””)にすることで、システムカタログに保存されない
‘ 「一時QueryDef」としてメモリ上にのみ生成される
Set qdf = db.CreateQueryDef(“”, dynamicSQL)
‘ パラメータの設定
qdf.Parameters(“p_Value”) = paramValue
‘ レコードセットとしての取得例
Dim rs As DAO.Recordset
Set rs = qdf.OpenRecordset(dbOpenSnapshot)
‘ 処理ループ (省略)
Do Until rs.EOF
‘ 処理…
rs.MoveNext
Loop
CleanUp:
If Not rs Is Nothing Then
rs.Close
Set rs = Nothing
End If
If Not qdf Is Nothing Then
‘ 一時QueryDefはCloseすることでメモリから完全に消去される
qdf.Close
Set qdf = Nothing
End If
Set db = Nothing
Exit Sub
ErrorHandler:
MsgBox “Error: ” & Err.Description, vbCritical
Resume CleanUp
End Sub
この「一時QueryDef」パターンを使えば、SQLインジェクションの耐性を完全に維持したまま、柔軟な動的クエリの構築と高速な実行計画の恩恵を同時に受けることができる。
—
総括
VBAは「おもちゃの言語」ではない。背後にあるDAOとJet/ACEデータベースエンジンは、正しく使えば極めて堅牢で高速なエンタープライズ級のデータ処理能力を発揮する。
文字列結合によるSQL構築という悪習を断ち切り、`DAO.QueryDef` によるパラメータバインディングと厳格なオブジェクトライフサイクル管理を導入すること。それこそが、レガシーシステムの寿命を延ばし、真のプロフェッショナルとしてシステムを掌握するための唯一の道である。
