【実務・中級編】QueryDefで実現する「固定クエリ」の動的書き換えテクニック – Access VBA解析バイブル

スポンサーリンク

Access VBAを掌握する極限の知見:QueryDefによる動的SQL書き換えの極意

開発現場でこんなコードを見たことはないだろうか。

‘ 【アンチパターン】VBA内にSQLを直書きする愚行
Dim strSQL As String
strSQL = “SELECT FROM T_受注明細 WHERE 顧客ID = ” & Me.txtCustomerID & ” AND 注文日 >= #” & Me.txtStartDate & “#”
Me.RecordSource = strSQL

一見すると動く。しかし、このアプローチをとった瞬間から、そのアプリケーションの保守性は死に体となる。エスケープ処理の漏れによるSQLインジェクションのリスク、長大なSQL文がVBAの文字列として迷子になる可読性の低さ、そして実行計画のキャッシュが効かないことによるパフォーマンスの劣化。

真にスケーラブルで堅牢なAccessシステムを構築したいのであれば、VBAから生のSQL文字列を追放しなければならない。

今回は、Accessが持つ隠れた(しかし最強の)エンジン機能である `QueryDef` オブジェクトを用いた「固定クエリの動的書き換え」 という設計術を授けよう。

—

なぜ「QueryDefの動的書き換え」なのか?

多くの初中級プログラマーは、SQLを動的に変えたい場合、前述のようにVBA内で文字列を結合するか、パラメータ付きクエリ(DAOのParameterオブジェクトやCommandオブジェクト)を複雑に組み立てようとする。

しかし、Access(Jet/ACEエンジン)の特性を理解していれば、答えは一つに絞られる。
「データベースにあらかじめ『器(QueryDef)』を用意し、VBAからはその `.SQL` プロパティだけを差し替える」 のだ。

この設計がもたらす圧倒的なメリット

1. SQLの完全な分離と可読性の向上
SQLはAccessのクエリデザイナ、またはビューアーでシンタックスハイライトされた状態で管理できる。複雑なJOINや集計関数も、VBAの迷宮から救い出される。
2. コンパイル時・設計時の恩恵
あらかじめクエリとして存在しているため、フィールド名の変更やテーブル構造の変更に対してAccessのデータベースドキュメント機能や依存関係チェックの恩恵を受けられる。
3. デバッグの容易性
VBA側でエラーが起きた際、最終的にどんなSQLがQueryDefに書き込まれたのかをナビゲーションウィンドウから直接開き、SQLビューで即座に検証・修正できる。

—

【アーキテクチャ設計】堅牢な動的書き換えの基本方針

プロダクション環境でこの手法を導入する際、以下の原則を遵守すること。

  • 一時クエリは使わず、専用の「作業用QueryDef」を物理的に定義する

コード内で `CreateQueryDef` を乱発してはならない。データベースの肥大化(Bloat)を招く原因となる。あらかじめ `qdef_DynamicSearch` のような名前の空のクエリを保存しておき、それを使い回す。

  • 値の埋め込みには細心の注意を払う(またはパラメータを併用する)

文字列を直接連結するのではなく、適切な型変換(特に日付と文字列のエスケープ)を行うか、QueryDefのパラメータ機能と組み合わせる。

—

実践!プロダクションコード例

それでは、実務でそのまま使える堅牢な実装コードを公開しよう。
ここでは、フォーム上の複数の検索条件(顧客名、担当者、日付範囲)に応じて、あらかじめ用意したQueryDefのSQLを動的に組み立て・書き換えて、帳票やサブフォームにバインドするシナリオを想定する。

前提条件

1. Accessのクエリ一覧に、あらかじめ `qdef_DynamicSearch` という名前のクエリ(中身は適当な `SELECT 1` などで良い)を作っておく。
2. フォームに検索条件を入力するコントロール(`txtCustomer`、`txtStaff`、`txtDateFrom`、`txtDateTo`)と、結果を表示するサブフォームまたはリストビューが存在する。

モジュールコード

Option Compare Database
Option Explicit

‘ =========================================================================
‘ 担当者名: 業務自動化アーキテクト
‘ 概要: QueryDefのSQLプロパティを動的に書き換え、安全かつ高速にデータを取得する
‘ =========================================================================
Public Sub ExecuteDynamicQuery()
On Error GoTo ErrorHandler

Dim db As DAO.Database
Dim qdef As DAO.QueryDef
Dim strBaseSQL As String
Dim strWhere As String
Dim strFinalSQL As String

Set db = CurrentDb()

‘ 1. ベースとなるSQLの定義(本来は別モジュールや定数、あるいは別の固定クエリから取得しても良い)
strBaseSQL = “SELECT T_Orders.OrderID, T_Orders.OrderDate, T_Customers.CustomerName, T_Orders.Amount, T_Orders.StaffName ” & _
“FROM T_Customers INNER JOIN T_Orders ON T_Customers.CustomerID = T_Orders.CustomerID”

strWhere = “”

‘ 2. フォームの入力値に応じたWHERE句の動的構築(安全な型考慮)

‘ 顧客名部分一致検索
If Not IsNull(Me.txtCustomer.Value) And Me.txtCustomer.Value <> “” Then
strWhere = AddCondition(strWhere, “T_Customers.CustomerName LIKE ‘” & EscapeSQLString(Me.txtCustomer.Value) & “‘”)
End If

‘ 担当者完全一致検索
If Not IsNull(Me.txtStaff.Value) And Me.txtStaff.Value <> “” Then
strWhere = AddCondition(strWhere, “T_Orders.StaffName = ‘” & EscapeSQLString(Me.txtStaff.Value) & “‘”)
End If

‘ 日付範囲(開始)
If Not IsNull(Me.txtDateFrom.Value) Then
‘ Access SQLでは日付は # で囲み、YYYY-MM-DD形式にフォーマットするのが鉄則
strWhere = AddCondition(strWhere, “T_Orders.OrderDate >= #” & Format(Me.txtDateFrom.Value, “yyyy\/mm\/dd”) & “#”)
End If

‘ 日付範囲(終了)
If Not IsNull(Me.txtDateTo.Value) Then
strWhere = AddCondition(strWhere, “T_Orders.OrderDate <= #" & Format(Me.txtDateTo.Value, "yyyy\/mm\/dd") & "#") End If ' 3. 最終的なSQLの組み立て strFinalSQL = strBaseSQL If strWhere <> “” Then
strFinalSQL = strFinalSQL & ” WHERE ” & strWhere
End If

‘ 順序の付与(必要に応じて)
strFinalSQL = strFinalSQL & ” ORDER BY T_Orders.OrderDate DESC;”

‘ 4. 【核心】QueryDefのSQLプロパティを書き換える
Set qdef = db.QueryDefs(“qdef_DynamicSearch”)
qdef.SQL = strFinalSQL

‘ 5. サブフォームのレコードソースにこのクエリを指定して再描画
‘ ※クエリ名義でバインドすることで、Accessエンジンは最適化されたパスを実行できる
Me.subGrid.Form.RecordSource = “qdef_DynamicSearch”
Me.subGrid.Form.Requery

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

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

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

‘ =========================================================================
‘ 補助関数: WHERE句の条件を安全に結合する
‘ =========================================================================
Private Function AddCondition(ByVal currentWhere As String, ByVal newCondition As String) As String
If currentWhere = “” Then
AddCondition = newCondition
Else
AddCondition = currentWhere & ” AND ” & newCondition
End If
End Function

‘ =========================================================================
‘ 補助関数: SQLインジェクション・構文エラーを防ぐための最低限のエスケープ
‘ =========================================================================
Private Function EscapeSQLString(ByVal val As String) As String
‘ シングルクォーテーションのエスケープ(’ -> ”)
EscapeSQLString = Replace(val, “‘”, “””)
End Function

—

開発現場で陥る「罠」とチーフからの忠告

この手法を実務導入する際、現場の開発者が必ずと言っていいほどハマる「落とし穴」がある。これを事前に防ぐのがプロの仕事だ。

1. データベースの肥大化(Bloat)に気をつけろ

前述したが、もしVBA内で `CurrentDb.CreateQueryDef(“”, strSQL)` のように名前のない一時クエリ(一時QueryDef)をループやイベントのたびに生成・破棄している場合、Accessのmdb/accdbファイルは凄まじい勢いで肥大化していく。
Jet/ACEエンジンは、一時オブジェクトの領域を完全に解放するのが下手くそだからだ。
「決まった名前のQueryDefを1つ用意し、そこに `.SQL = …` で上書きし続ける」 この鉄則を絶対に忘れるな。

2. マルチユーザー環境(フロントエンド・バックエンド分離)における注意点

もしあなたが、Accessファイルを「フロントエンド(UI/コード)」と「バックエンド(データ)」に綺麗に分割しているとする。
ここで注意すべきは、書き換えるQueryDefは「フロントエンド側」に存在していなければならないという点だ。
リンクテーブルに対してQueryDefのSQL書き換えはできない。フロントエンドに置いた作業用QueryDef(ローカルクエリ)のSQLを書き換え、その中でリンクテーブル(バックエンドのテーブル)を参照する構造にすること。これがマルチユーザー環境で破綻しないための絶対条件である。

—

結びにかえて

Access VBAは、しばしば「おもちゃの言語」「場当たり的なマクロの延長」と揶揄される。だがそれは、書く人間の設計思想が場当たり的であるからに他ならない。

今回紹介した QueryDefによる動的SQL書き換え は、Accessのデータベースエンジンの挙動を深く理解し、パフォーマンスと保守性を高次元で両立させるためのプロフェッショナル・スタンダードだ。

あなたの書くコードから生の長大SQL文字列を排除し、美しく、拡張性のあるシステムへと昇華させてほしい。健闘を祈る。

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