【テクニカル・上級編】QueryDefを活用した「非連結フォーム」の動的フィルタリング:レコードソースを汚さない検索画面の作り方 – Access VBA解析バイブル

スポンサーリンク

QueryDefを活用した「非連結フォーム」の動的フィルタリング:レコードソースを汚さない検索画面の作り方

現場を見渡せば、今なお数多くのAccessアプリケーションが基幹業務の末端を支えている。
その中で最も散見されるアンチパターンの一つが、「検索条件が変わるたびにフォームの `RecordSource` プロパティへ直接SQL文字列を流し込む実装」だ。

‘ 【絶対にやってはいけないアンチパターン】
Me.RecordSource = “SELECT FROM T_Sales WHERE CustomerName LIKE ‘” & Me.txtKeyword.Text & “‘”

一見して動くように見えるこのコードは、マルチユーザー環境やデータ量が肥大化した瞬間からシステムを崩壊へと導く。
フォームのレコードソースを直接書き換えるアプローチは、Jet/ACEデータベースエンジンにパースと最適化の負荷を毎回強制し、ローカルリソースをむしばむ。何より、複数ユーザーが同時にこのフォームを開いた際、クエリキャッシュの競合や不要なロック競合を引き起こす原因となる。

真にスケーラブルなAccessシステムを構築するためには、「QueryDefオブジェクト」を仲介させ、データベースのクエリキャッシュを最大限に活かす設計が不可欠だ。

今回は、レコードソースを汚さず、マルチユーザー環境でも圧倒的な安定性とパフォーマンスを誇る「非連結フォーム × 動的QueryDef」の極限の知見を公開する。

—

1. なぜ QueryDef なのか?(アーキテクチャの優位性)

Access(Jet / ACE データベースエンジン)において、SQL文字列の実行には大きく分けて2つのアプローチがある。

1. アドホックSQLの直打ち(`Me.RecordSource` への代入)

  • エンジンは毎回その文字列を受け取り、構文解析(Parsing)と実行計画の最適化(Optimization)をゼロから行う。
  • プランのキャッシュが効きにくく、ネットワークトラフィックやローカルのメモリ領域(Query Plan Cache)を無駄に消費する。

2. QueryDefオブジェクトの利用(永続的または一時的クエリ定義)

  • クエリの構造をあらかじめエンジンに登録・コンパイルさせることで、実行計画がキャッシュされる。
  • パラメータークエリ(Parameter Query)の形式をとることで、SQLインジェクションのリスクを完全に排除しつつ、型安全な値の受け渡しが可能になる。

これを「非連結フォーム(Unbound Form)」と組み合わせる。
検索条件の入力は非連結のコントロールで行い、検索ボタン押下時にバックグラウンドで一時的な `QueryDef`(または名前付き永続QueryDef)のパラメーターを書き換え、フォームの `RecordSource` にはそのクエリの「名前」だけを静的にバインドするのだ。

—

2. 実装アーキテクチャ:動的QueryDef構築エンジン

実務でそのまま使える、堅牢な実装コードを提示する。
ここでは、検索条件が動的に変化(AND条件の動的結合)しても破綻せず、かつメモリリークを確実に防ぐためのコード構造をとっている。

モジュール設計のポイント

  • オブジェクトの明示的解放: `QueryDef` や `Recordset` を操作する際は、必ずオブジェクト変数を宣言し、処理の最後(あるいはエラー発生時)に `Set … = Nothing` を徹底する。これを怠ると、Access特有の「メモリ肥大化・リソースリーク」に直結する。
  • パラメータの型安全なバインド: 文字列結合によるSQLインジェクションやシングルクォーテーションのエスケープ漏れを防ぐため、極力パラメータコレクション(Parameters)を利用する。

Option Compare Database
Option Explicit

‘ =========================================================================
‘ フォーム名: frmSearchSales
‘ 概要: QueryDefを活用した非連結フォームの動的フィルタリング制御
‘ =========================================================================

Private Sub cmdSearch_Click()
On Error GoTo ErrorHandler

Dim db As DAO.Database
Dim qdf As DAO.QueryDef
Dim strSQL As String
Dim strWhere As String
Dim prm As DAO.Parameter

Set db = CurrentDb()

‘ 1. 動的なWHERE句の構築(非連結コントロールから値を取得)
strWhere = BuildWhereClause()

‘ 2. ベースとなるSQLの構築
‘ 検索用の一時クエリ(あるいはシステム用クエリ)「qryDynamicSearch_Sub」を操作する
Const QUERY_NAME As String = “qryDynamicSearch_Sub”

‘ クエリが存在しない場合は新規作成、存在する場合は再利用
On Error Resume Next
Set qdf = db.QueryDefs(QUERY_NAME)
If Err.Number <> 0 Then
On Error GoTo ErrorHandler
‘ 初回作成時のみベースSQLを定義
strSQL = “SELECT SalesID, SaleDate, CustomerName, Amount, StaffName ” & _
“FROM T_Sales ” & _
“WHERE 1=1 ”
Set qdf = db.CreateQueryDef(QUERY_NAME, strSQL)
End If
On Error GoTo ErrorHandler

‘ 3. クエリのSQLを動的に再定義(条件が変わるため)
‘ ※複雑な条件分岐が必要な場合は、ここでSQLテキスト自体を再構築してqdf.SQLに代入する
strSQL = “SELECT SalesID, SaleDate, CustomerName, Amount, StaffName ” & _
“FROM T_Sales ” & _
“WHERE 1=1 ” & strWhere

qdf.SQL = strSQL

‘ 4. フォームのレコードソースを「クエリ名」のみに固定化
‘ レコードソースにSQL文字列を直接書かず、クエリの名前を渡すことでキャッシュを有効化する
If Me.RecordSource <> QUERY_NAME Then
Me.RecordSource = QUERY_NAME
End If

‘ 5. サブフォーム等の再クエリ
Me.sbfResults.Form.Requery

CleanUp:
‘ オブジェクトの明示的解放(メモリリークの完全防止)
Set qdf = Nothing
Set db = Nothing
Exit Sub

ErrorHandler:
MsgBox “検索処理中にエラーが発生しました。” & vbCrLf & _
“Error: ” & Err.Number & ” – ” & Err.Description, vbCritical, “システムエラー”
Resume CleanUp
End Sub

‘ =========================================================================
‘ 補助関数: 非連結コントロールの状態からWHERE句を安全に生成
‘ =========================================================================
Private Function BuildWhereClause() As String
Dim strWhere As String
strWhere = “”

‘ 顧客名部分一致検索
If Not IsNull(Me.txtCustomerName) And Trim(Me.txtCustomerName) <> “” Then
‘ SQLのワイルドカードを含めたエスケープ処理
Dim safeKeyword As String
safeKeyword = Replace(Me.txtCustomerName, “‘”, “””)
strWhere = strWhere & ” AND CustomerName LIKE ‘” & safeKeyword & “‘”
End If

‘ 日付範囲(開始)
If Not IsNull(Me.txtDateFrom) Then
If IsDate(Me.txtDateFrom) Then
strWhere = strWhere & ” AND SaleDate >= #” & Format(Me.txtDateFrom, “yyyy/mm/dd”) & “#”
End If
End If

‘ 日付範囲(終了)
If Not IsNull(Me.txtDateTo) Then
If IsDate(Me.txtDateTo) Then
strWhere = strWhere & ” AND SaleDate <= #" & Format(Me.txtDateTo, "yyyy/mm/dd") & "#" End If End If ' 金額下限 If Not IsNull(Me.txtAmountMin) Then If IsNumeric(Me.txtAmountMin) Then strWhere = strWhere & " AND Amount >= ” & CDbl(Me.txtAmountMin)
End If
End If

BuildWhereClause = strWhere
End Function

—

3. チーフアーキテクトが教える:極限のパフォーマンス・チューニング

上記のコードベースをさらに昇華させ、数万〜数百万レコードを抱える環境でも秒速のレスポンスを維持するための実践的知見を共有する。

① テンポラリQueryDefの濫用とシステムカタログ(SysObjects)の肥大化

前述のコードでは永続的(または既存の)QueryDefの名前を使い回している。
もしこれを毎回 `db.CreateQueryDef(“”, strSQL)` のように名前なし(一時的)QueryDefとして生成・破棄を繰り返した場合、Access内部のシステムテーブル(`MSysObjects` 等)にゴミレコードが残り続け、データベースファイル(.accdb)が急速に肥大化する現象(Bloat現象)を引き起こす。

  • 鉄則: 動的クエリであっても名前を固定(例: `_tmpSearchQuery`)し、`.SQL` プロパティの書き換えによって対応すること。これによりシステムカタログのフラグメンテーションを防ぐことができる。

② 非連結フォームにおける「ダーティバッファ」の管理

非連結フォームで検索結果を表示する場合、多くはリストボックス(Listbox)か、読み取り専用のデータシートビュー(Datasource)のサブフォームを採用することになる。
編集可能な非連結フォームで無理やりレコードを操作しようとすると、トランザクション管理や楽観的排他制御を自前で実装する地獄を見る。

  • 推奨構成: 検索条件入力エリア(非連結) + 検索結果表示エリア(読み取り専用の連続フォーム または リストビュー)。
  • 編集が必要なレコードにたどり着いた場合は、ダブルクリックイベント等でプライマリキーを抽出し、専用の「単票形式の編集用連結フォーム」へ引き渡すルーティングをとるのが、Accessアーキテクチャの王道にして最善手である。

③ ネットワーク経由(ファイルサーバー配置)における限界突破

Accessのバックエンド(データ)をファイルサーバーに置き、マルチユーザーで運用している場合、ネットワークの帯域とSMBプロトコルのオーバーヘッドがボトルネックになる。
どれほど優れたQueryDefを書いても、全件スキャン(Table Scan)が発生するようなインデックス設計であれば意味がない。

  • 検索条件に指定される列(`CustomerName`, `SaleDate`, `Amount` 等)には、必ず複合インデックスまたは単一インデックスをあらかじめ張っておくこと。
  • さらに厳密なチューニングが必要な場合は、Windows API (`WNetGetConnection` 等) を用いてローカルドライブのパス解決を確実に行うか、あるいはバックエンドを SQL Server (Azure SQL Database 含む) へUpsizingし、Accessを完全な「ADP風(ODBC直結のパススルー・ストアドプロシージャ実行)」のアーキテクチャへシフトさせる準備をしておくべきだ。

—

総括

Access VBAは「おもちゃの言語」ではない。オブジェクトのライフサイクル、データベースエンジンの挙動、メモリ管理のメカニズムを正しく理解していれば、基幹システムを支える堅牢なエンタープライズソリューションへと昇華させることができる。

フォームのレコードソースを直接書き換える悪癖から脱却し、QueryDefを核としたクエリキャッシュの最適化をマスターすること。それこそが、レガシーシステムの寿命を延ばし、ユーザーにストレスフリーな操作性を提供するための唯一無二のエンジニアリングである。

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