【実務・中級編】CurrentDb.QueryDefsのSQLプロパティを書き換えて実現する「動的SQL」のセキュリティ – Access VBA解析バイブル

スポンサーリンク

【Access VBA極限解説】CurrentDb.QueryDefsの動的SQL書換えにおける「真のセキュリティ」と堅牢な設計

現場のプログラマーから、よくこんな質問を受ける。
「ユーザーの入力条件に合わせて、クエリのSQLをVBAから書き換えて実行したいのですが、文字列連結でSQLを作るとセキュリティ上ヤバいですよね? でも、パラメータクエリだと画面の複雑な条件分岐に対応しきれないんです」

よくぞ聞いてくれた。この課題は、Access VBAで業務システムを構築する者が必ず直面する「利便性と安全性のジレンマ」だ。

結論から言おう。文字列連結による動的SQLの構築は、SQLインジェクションの温床であり、Access開発においても絶対に排除すべき悪手である。しかし、パラメータクエリ(`Parameters`コレクション)を正しく使いこなし、かつ安全に`QueryDefs`のSQLプロパティを制御する設計手法をマスターすれば、柔軟性と堅牢性を完全に両立させることができる。

今回は、Accessのオブジェクトモデルの裏側まで知り尽くしたアーキテクトの視点から、`CurrentDb.QueryDefs`を安全に極限まで使い倒すベストプラクティスを伝授する。

1. なぜ「文字列の直叩き(連結)」は地雷なのか?

多くの初学者、いや、中堅プログラマーでさえも、次のようなコードを書く。

‘ 【アンチパターン】絶対にやってはいけない文字列連結
Dim strSQL As String
strSQL = “SELECT FROM T_受注 WHERE 顧客名 = ‘” & Me.txtKeyword.Value & “‘”
CurrentDb.QueryDefs(“qrySearch”).SQL = strSQL

一見、動的にクエリが変わって便利に見えるだろう。だが、これには致命的な問題が3つある。

1. SQLインジェクションの脅威
ユーザーが入力欄に `’ OR ‘1’=’1` のような悪意ある文字列を入れた瞬間、意図しない全件データが流出する。社内ニッチなAccessアプリだからと油断してはならない。
2. クエリのキャッシュ汚染とパフォーマンス劣化
Access(Jet/ACEエンジン)はSQLの実行計画をキャッシュする。値が変わるたびにSQL文字列そのものを書き換えると、エンジンは毎回新しいSQLと認識し、実行計画の再生成(パース)コストを支払い続けることになる。これはマルチユーザー環境でのパフォーマンス急低下を引き起こす。
3. 型変換とエスケープのエラー地獄
日付型や数値型を文字列結合しようとすると、`#` や `’` のクォーテーションの囲み忘れによる構文エラーや、環境による日付フォーマット(ISO 8601等)の不一致バグが頻発する。

では、どうすべきか?
答えは「構造(SQL文)とデータ(パラメータ)の完全分離」である。

2. 堅牢な設計:パラメータクエリをコードから安全に操作する

Accessの`QueryDef`オブジェクトは、実はSQL文の中にパラメータの型定義(`PARAMETERS`句)を保持できる。これを利用するのがプロフェッショナルのやり方だ。

あらかじめクエリの定義(`QueryDefs`)側には、プレースホルダーや明示的なパラメータ型を定義しておき、VBA側からは安全にパラメータ値をバインドする。

しかし、「検索条件の数や組み合わせが動的に変わる(例:日付範囲も指定するし、担当者も絞るかもしれない)」という要件の場合はどうするか?
ここで、「ベースとなる安全なSELECT句・FROM句を維持しつつ、WHERE句の条件項目をパラメータ付きで安全に組み立てる」というアプローチが必要になる。

3. 【プロダクションコード】安全な動的SQL構築のベストプラクティス

以下に、現場の商用システムでそのまま使える、堅牢でクリーンなVBAコードを提示する。
このコードでは、`QueryDefs`のSQLを直接書き換えるのではなく、「安全なパラメータ付きクエリを動的に定義(または更新)し、ADO/DAOのパラメータバインド機構を最大限に活かす」設計をとっている。

Option Explicit

‘ ==============================================================================
‘ 担当者・アーキテクト特製の安全な動的クエリ生成・実行モジュール
‘ ==============================================================================
Public Sub ExecuteSecureDynamicQuery()
On Error GoTo ErrorHandler

Dim db As DAO.Database
Dim qdf As DAO.QueryDef
Dim strSQL As String
Dim prmKeyword As String
Dim hasDateCondition As Boolean

Set db = CurrentDb()
Const QUERY_NAME As String = “qryDynamicSearch_Temp”

‘ ————————————————————————–
| 1. 入力値の取得とバリデーション(ここで不正な型や値を弾く)
‘ ————————————————————————–
If IsNull(Me.txtKeyword) Then
prmKeyword = “”
Else
‘ 特殊文字や危険な文字列のトリミング(必要に応じた無害化)
prmKeyword = Trim$(Me.txtKeyword.Value)
End If

hasDateCondition = Not (IsNull(Me.txtDateFrom) Or IsNull(Me.txtDateTo))

‘ ————————————————————————–
| 2. SQLの骨組み(構造)を構築
‘ ※ 値を直接埋め込まず、パラメータのプレースホルダー([prm1]など)を使用する
‘ ————————————————————————–
strSQL = “PARAMETERS [prmKeyword] Text(255)”

If hasDateCondition Then
strSQL = strSQL & “, [prmDateFrom] DateTime, [prmDateTo] DateTime”
End If
strSQL = strSQL & ” ”

strSQL = strSQL & “SELECT FROM T_受注 ” & _
“WHERE (T_受注.顧客名 LIKE ” & [prmKeyword] & ” OR [prmKeyword] = ”)”

If hasDateCondition Then
strSQL = strSQL & ” AND (T_受注.受注日 BETWEEN [prmDateFrom] AND [prmDateTo])”
End If

‘ ————————————————————————–
| 3. QueryDefsへの安全な登録(一時クエリの作成または上書き)
‘ ————————————————————————–
‘ 既存の同名クエリがあれば削除、なければ新規作成
On Error Resume Next
db.QueryDefs.Delete QUERY_NAME
On Error GoTo ErrorHandler

Set qdf = db.CreateQueryDef(QUERY_NAME, strSQL)

‘ ————————————————————————–
| 4. パラメータへの値のバインド(型安全性の確保)
‘ ————————————————————————–
qdf.Parameters(“[prmKeyword]”).Value = prmKeyword

If hasDateCondition Then
qdf.Parameters(“[prmDateFrom]”).Value = Me.txtDateFrom.Value
qdf.Parameters(“[prmDateTo]”).Value = Me.txtDateTo.Value
End If

‘ ————————————————————————–
| 5. フォームやレポートへのバインド、またはレコードセットとしての処理
‘ ————————————————————————–
‘ 例:フォームのレコードソースに安全なクエリを指定
Me.Form.RecordSource = QUERY_NAME

‘ クリーンアップ
Set qdf = Nothing
Set db = Nothing
Exit Sub

ErrorHandler:
MsgBox “予期せぬエラーが発生しました。” & vbCrLf & _
“エラー番号: ” & Err.Number & vbCrLf & _
“エラー内容: ” & Err.Description, vbCritical, “システムエラー”

If Not qdf Is Nothing Then Set qdf = Nothing
If Not db Is Nothing Then Set db = Nothing
End Sub

4. チーフアーキテクトからの設計上の注意点と知見

上記のコードを見て、「なぜわざわざ一時クエリを作っているのか?」と思った読者もいるだろう。そこにこそ、Access開発の極意がある。

① `CurrentDb` のライフサイクルに注意せよ

Access VBAで頻出する `CurrentDb.QueryDefs(…)` という書き方は、呼び出すたびに一時的なDatabaseオブジェクトのインスタンスを生成する。
ループ処理の中で `CurrentDb` を乱用すると、メモリリークやJetエンジンへの負荷増大を招く。上記のコードのように、一度 `Set db = CurrentDb()` と変数に受けてから操作し、最後に解放する(`Set db = Nothing`)のが正しいオブジェクトライフサイクルの作法だ。

② クエリのゴミ屋敷化を防ぐ

コード内で一時クエリ(`qryDynamicSearch_Temp`など)を動的生成する場合、アプリケーション終了時や処理の途中でゴミクエリがデータベース内に残骸として残り、ファイルサイズが膨れ上がる原因になる。
マルチユーザー環境(バックエンド・フロントエンド分離構成)のフロントエンド側であっても、不要になった一時オブジェクトのクリーンアップは設計の義務である。本番運用では、永続的なクエリにあらかじめパラメータを定義しておき、`QueryDefs(“qryStatic”).Parameters(“…”).Value = …` のようにSQL文字列そのものを書き換えない設計にするのが最上位のベストプラクティスだ。

5. おわりに

「動的SQL」という言葉の響きには、どこか魔術的なカッコよさがある。しかし、その裏側でセキュリティホールを掘ったり、パフォーマンスを自らブチ壊したりしている現場を数多く見てきた。

Access VBAといえども、立派なデータベースアプリケーションの構築手法である。
構造とデータを分離し、型安全なパラメータバインドを徹底すること。この基本を愚直に守るだけで、あなたの作る業務システムは、何年経っても壊れない「堅牢なインフラ」へと生まれ変わる。

明日からのコードで、ぜひこの設計思想を取り入れてみてほしい。プロとしての仕事の質が、確実に一段階上がるはずだ。

タイトルとURLをコピーしました