【Access VBA】SQLインジェクションを根絶し、保守性を極限まで高める「動的クエリビルダー」設計論
開発現場でこんなコードを目撃したことはないか?
‘ 駄目なコードの典型例
Dim sql As String
sql = “SELECT FROM T_Sales WHERE 1=1 ”
If Not IsNull(Me.txtCustomer) Then
sql = sql & “AND CustomerName LIKE ‘%” & Me.txtCustomer & “%’ ”
End If
If Not IsNull(Me.txtDateFrom) Then
sql = sql & “AND SaleDate >= #” & Me.txtDateFrom & “# ”
End If
‘ 以下、延々と続く文字列結合の泥沼…
おいおい、待ってくれ。文字列を直接結合してSQLを組み立てるこの手法は、SQLインジェクションの温床になるだけでなく、シングルクォーテーションのミス、日付の書式(USフォーマット問題)、そしてNull値の混入による実行時エラーを引き起こす「百害あって一利なし」のアンチパターンだ。
真にプロフェッショナルなAccess開発者であれば、クエリの構築は「文字列結合」ではなく、「オブジェクト指向的なパラメータ管理」で行うべきだ。
今回は、実務の現場で複雑怪奇な検索条件に頭を悩ませているあなたへ贈る、再利用可能かつ堅牢な「クエリビルダークラス(clsQueryBuilder)」の設計手法を完全伝授する。
—
なぜ「クエリビルダークラス」が必要なのか?
Access VBAで大規模な検索画面や帳票出力ロジックを作るとき、条件分岐が複雑化してコードがスパゲッティ化するのはお約束だ。
これを解消するためには、以下の要件を満たすアーキテクチャが必要になる。
1. SQL文とパラメータの完全分離: パラメータをQueryDefのParametersコレクションにバインドし、型安全性を担保する。
2. 可変長条件のスマートな管理: 条件の有無を意識せず、追加(Add)していくだけでWHERE句が自動構築される仕組み。
3. リソースの適切な管理: 生成した一時QueryDefやRecordsetのメモリリークを防ぐライフサイクル管理。
これらを美しくカプセル化するのが、今回作成する`clsQueryBuilder`である。
—
プロダクションコード:実装の全貌
以下の2つのモジュールをVBE(Visual Basic Editor)にインポートしてほしい。
1. クラスモジュール:`clsQueryBuilder`
条件の保持、SQLの組み立て、そして安全なQueryDefの生成を担当する中核エンジンだ。
‘ =================================================================
‘ クラス名: clsQueryBuilder
‘ 概要: 複雑な抽出条件を動的に構築し、パラメータ付きQueryDefを生成する
‘ =================================================================
Option Compare Database
Option Explicit
‘ 内部保持用コレクション
Private m_Conditions As Collection
Private m_Parameters As Collection
Private m_BaseTable As String
Private m_SelectFields As String
‘ コンストラクタ
Private Sub Class_Initialize()
Set m_Conditions = New Collection
Set m_Parameters = New Collection
m_SelectFields = “”
m_BaseTable = “”
End Sub
‘ デストラクタ
Private Sub Class_Terminate()
Set m_Conditions = Nothing
Set m_Parameters = Nothing
End Sub
‘ ベースとなるテーブル、またはクエリの設定
Public Property Let BaseSource(ByVal sourceName As String)
m_BaseTable = sourceName
End Property
‘ 取得フィールドの設定(省略時は “”)
Public Property Let SelectFields(ByVal fields As String)
m_SelectFields = fields
End Property
‘ 条件の追加
‘ @param fieldName フィールド名
‘ @param operator 演算子(”=”, “LIKE”, “>=”, “<=", "IN" など)
' @param value 比較値
Public Sub AddCondition(ByVal fieldName As String, ByVal op As String, ByVal value As Variant)
' 値がNullまたは長さ0の文字列の場合は条件に追加しない(柔軟なオプショナル検索)
If IsNull(value) Then Exit Sub
If VarType(value) = vbString Then
If Trim(value) = "" Then Exit Sub
End If
Dim paramName As String
' パラメータ名は一意になるようプレフィックスと連番を付与
paramName = "p_" & m_Conditions.Count + 1
' 演算子がLIKEの場合のワイルドカード付与をケア
Dim actualValue As Variant
actualValue = value
If UCase(Trim(op)) = "LIKE" Then
' 部分一致を標準とする
actualValue = "%" & value & "%"
End If
m_Conditions.Add fieldName & " " & op & " [" & paramName & "]"
m_Parameters.Add Array(paramName, actualValue)
End Sub
' 動的SQLの構築とQueryDefの生成
' @param qdfName 保存するQueryDef名(空欄の場合は一時クエリとして扱う)
Public Function BuildQueryDef(Optional ByVal qdfName As String = "") As QueryDef
If m_BaseTable = "" Then
Err.Raise 9999, "clsQueryBuilder", "BaseSourceが設定されていません。"
End If
Dim sql As String
sql = "SELECT " & m_SelectFields & " FROM " & m_BaseTable
' WHERE句の構築
If m_Conditions.Count > 0 Then
sql = sql & ” WHERE ” & JoinCollection(m_Conditions, ” AND “)
End If
Dim db As DAO.Database
Set db = CurrentDb
Dim qdf As DAO.QueryDef
‘ 既存の同名QueryDefがあれば削除
On Error Resume Next
db.QueryDefs.Delete qdfName
On Error GoTo 0
If qdfName <> “” Then
‘ 永続クエリとして作成
Set qdf = db.CreateQueryDef(qdfName, sql)
Else
‘ 名前なし(一時)クエリとして作成
Set qdf = db.CreateQueryDef(“”, sql)
End If
‘ パラメータのバインド
Dim paramInfo As Variant
Dim i As Long
For i = 1 To m_Parameters.Count
paramInfo = m_Parameters(i)
‘ QueryDefのParametersコレクションに値を安全に設定
qdf.Parameters(CStr(paramInfo(0))) = paramInfo(1)
Next i
Set BuildQueryDef = qdf
Set db = Nothing
End Function
‘ ヘルパー関数: コレクションの要素を指定の区切り文字で結合
Private Function JoinCollection(ByVal col As Collection, ByVal delimiter As String) As String
Dim result As String
Dim item As Variant
Dim i As Long
For i = 1 To col.Count
If i = 1 Then
result = col(i)
Else
result = result & delimiter & col(i)
End If
Next i
JoinCollection = result
End Function
—
2. 呼び出し側サンプル:標準モジュール
フォームからの入力を想定し、実際にクエリビルダを使ってレコードセットを取得・走査するコードだ。
‘ =================================================================
‘ 実行モジュール例
‘ =================================================================
Option Compare Database
Option Explicit
Public Sub TestQueryBuilder()
Dim qb As clsQueryBuilder
Set qb = New clsQueryBuilder
‘ 1. 基本設定
qb.BaseSource = “T_Orders”
qb.SelectFields = “OrderID, CustomerID, OrderDate, TotalAmount”
‘ 2. 検索条件の動的追加(画面の入力値に見立てる)
‘ 顧客IDが入力されていれば条件に追加
Dim inputCustomerID As Variant
inputCustomerID = “CUST-001” ‘ フォームのテキストボックス等から取得
qb.AddCondition “CustomerID”, “=”, inputCustomerID
‘ 注文日(開始)
Dim inputDateFrom As Variant
inputDateFrom = #2023/04/01#
qb.AddCondition “OrderDate”, “>=”, inputDateFrom
‘ 担当者名(部分一致)
Dim inputStaffName As Variant
inputStaffName = “佐藤”
qb.AddCondition “StaffName”, “LIKE”, inputStaffName
‘ 3. QueryDefの生成(パラメータが安全にバインドされる)
Dim qdf As DAO.QueryDef
Set qdf = qb.BuildQueryDef()
‘ 4. レコードセットの取得と処理
Dim rs As DAO.Recordset
Set rs = qdf.OpenRecordset(dbOpenSnapshot)
Do Until rs.EOF
Debug.Print “注文ID: ” & rs(“OrderID”) & ” / 金額: ” & rs(“TotalAmount”)
rs.MoveNext
Loop
‘ 5. クリーンアップ
rs.Close
Set rs = Nothing
Set qdf = Nothing
Set qb = Nothing
Debug.Print “クエリの実行が正常に完了しました。”
End Sub
—
現場のアーキテクトが教える「実装の急所」
この設計を採用するにあたり、現場で必ず直面するポイントを先回りして解説しておこう。
① パラメータクエリの「型」の罠
Access(DAO)のパラメータクエリは非常に優秀だが、バインドする値のデータ型がテーブル側のフィールド定義と厳密に一致している必要がある。
例えば、`OrderDate`フィールドが日付/時刻型である場合、`#2023/04/01#`のように正しく日付型(Date型)として渡さなければならない。文字列 `”2023/04/01″` をそのまま渡すと型不一致エラーになる。
今回のクラスでは `Variant` 型で値を受け取り、呼び出し元で適切な型(Long, Currency, Dateなど)にキャストして渡す設計にしているため、このトラブルを未然に防げる。
② LIKE検索とワイルドカードのスコープ
SQL文の文字列結合でLIKEを使う場合、`LIKE ‘” & val & “‘”` のようにアスタリスク(“)を記述するのがAccess(Jet/ACE OLEDB)の流儀だが、ADOや将来的なSQL Serverへの移行(UPSIZING)を見据えると、ANSI標準のパーセント(`%`)を使うべきだ。
上記の `clsQueryBuilder` では、内部的に `%` を使いつつ、DAOのパラメータとして安全に処理させているため、将来的なDBバックエンドの変更にも強い。
③ メモリリークとパフォーマンス
`db.CreateQueryDef(“”, sql)` で作成される名前なしクエリ(一時クエリ)は、インスタンスが破棄されるかプログラムが終了するまでメモリ上に残る。
大量の動的クエリをループ内で数千回生成するような愚行は避けるべきだ。検索ボタンのクリックイベントなど、ユーザーアクションのトリガーごとに1回生成・破棄するライフサイクルであれば、メモリリークの心配はない。
—
まとめ:プロたる者、コードの「品格」にこだわれ
「動的にSQLを作る=文字列をガチャガチャ繋げる」という悪習は、今日で終わりにしてほしい。
今回紹介した「クエリビルダークラス」をあなたのプロジェクトに導入すれば、以下のメリットがもれなく手に入る。
- セキュリティ: SQLインジェクションの完全無効化
- 保守性: 条件追加・変更時の影響範囲の局所化
- 可読性: 「何をやっているか」が一目でわかるエレガントなVBAコード
実務で信頼されるシステムとは、動くという事実だけでなく、こうした「将来の改修に耐えうる堅牢な設計」の上に成り立っている。ぜひ、あなたの現場のAccessアプリに組み込み、その圧倒的な安定性を体感してほしい。
