【テクニカル・上級編】フォームの「RecordSource」をVBAで動的に書き換える検索画面の構築 – Access VBA解析バイブル

スポンサーリンク

Access VBAを掌握する極限の知見:動的RecordSource制御による高速検索画面のアーキテクチャ

レガシーシステムの寿命は、しばしば「検索画面のパフォーマンス」と「保守性」によって決まる。
Accessをフロントエンドに据えたクライアント・サーバー(またはローカルファイル)構成において、数万〜数百万件のレコードをさばく検索画面を作る時、初学者が陥る罠が「全件読み込みローカルフィルタリング」や「非効率なクエリの直書き」だ。

本稿では、フォームの `RecordSource` をVBAで動的に書き換える手法を軸に、メモリのライフサイクル、JET/ACEデータベースエンジンの挙動、そして実務で耐えうる堅牢な検索画面の構築手法を、チーフアーキテクトの視点から徹底的に解説する。

1. なぜ「動的RecordSource」なのか?

検索条件に応じてクエリの定義を毎回デザインビューで変更するのは、プログラミングのアンチパターンにおける最上位に位置する。オブジェクトの破損(腐敗)を招くし、マルチユーザー環境では致命的な排他制御の競合を引き起こす。

真にスケーラブルなアプローチは、「ベースとなるSQL文字列をVBAで組み立て、フォームの `RecordSource` プロパティへ直接流し込む」ことだ。

しかし、これを雑に実装すると、SQLインジェクションのリスク、型の不一致によるJet/ACEのクエリ最適化失敗、そして何よりメモリリークと肥大化(Bloat)を引き起こす。このトレードオフをどう制圧するか。それが本稿の核心である。

2. 実装アーキテクチャ:動的SQL生成と安全な型ハンドリング

以下のコードは、単なる文字列連結によるSQL生成ではない。SQLインジェクションを完全に防ぎ、JET/ACEエンジンが最適化しやすいパラメータ構造を意識した、プロダクション品質の動的検索ロジックだ。

Option Compare Database
Option Explicit

‘ ==============================================================================
‘ フォーム名: frmSearch
‘ 概要: 検索条件をもとにRecordSourceを動的に構築し、高速な絞り込みを行う
‘ ==============================================================================

Private Sub cmdSearch_Click()
On Error GoTo ErrorHandler

Dim sqlBase As String
Dim sqlWhere As String
Dim sqlFinal As String
Dim db As DAO.Database
Dim qdf As DAO.QueryDef

‘ 1. 画面の再描画を停止し、ちらつきと不要なUIイベントを抑制
Application.Echo False
Me.Painting = False

Set db = CurrentDb()

‘ 2. ベースクエリの定義(ビューやストアドの代わりとして最適化されたSELECT句)
sqlBase = “SELECT CustomerID, CompanyName, ContactName, Phone, CreatedDate ” & _
“FROM m_Customers ”

sqlWhere = BuildWhereClause()

‘ 3. WHERE句が存在する場合は結合
If sqlWhere <> “” Then
sqlFinal = sqlBase & “WHERE ” & sqlWhere & ” ORDER BY CustomerID;”
Else
sqlFinal = sqlBase & “ORDER BY CustomerID;”
End If

‘ 【極限の知見】
‘ RecordSourceに直接長大なSQLを代入するのではなく、一時的なQueryDef(またはパラメータクエリ)
‘ を経由するか、直接代入するかはデータ量に依存する。
‘ 数千件程度であれば直接代入で十分だが、複雑なサブクエリを含む場合は
‘ あらかじめ用意した保存済みクエリのSQLプロパティを書き換える方が、
‘ アクセスの実行計画キャッシュにおいて有利な場合がある。

Me.RecordSource = sqlFinal
Me.Requery

‘ 4. 検索結果のレコード数に応じたハンドリング
If Me.Recordset.RecordCount = 0 Then
MsgBox “該当するデータが存在しません。”, vbInformation, “検索結果”
End If

CleanUp:
‘ 5. オブジェクトの明示的解放(VBAのガベージコレクタを信用するな)
If Not qdf Is Nothing Then
qdf.Close
Set qdf = Nothing
End If
Set db = Nothing

Me.Painting = True
Application.Echo True
Exit Sub

ErrorHandler:
Application.Echo True
Me.Painting = True
MsgBox “予期せぬエラーが発生しました。” & vbCrLf & _
“Error No: ” & Err.Number & vbCrLf & _
“Description: ” & Err.Description, vbCritical, “システムエラー”
Resume CleanUp
End Sub

‘ ==============================================================================
‘ 補助関数: UIの入力値から安全なWHERE句を動的に構築する
‘ ==============================================================================
Private Function BuildWhereClause() As String
Dim conditions As String
conditions = “”

‘ テキストボックス(部分一致・SQLインジェクション対策としてのエスケープ処理)
If Not IsNull(Me.txtCompanyName) And Trim(Me.txtCompanyName) <> “” Then
‘ シングルクォーテーションのサニタイジング
Dim safeValue As String
safeValue = Replace(Me.txtCompanyName, “‘”, “””)
conditions = AddCondition(conditions, “CompanyName LIKE ‘” & safeValue & “‘”)
End If

‘ 日付範囲(From)
If Not IsNull(Me.txtDateFrom) Then
‘ Jet/ACEは日付を # で囲む必要がある。フォーマットの厳密な統制が必須。
conditions = AddCondition(conditions, “CreatedDate >= #” & Format(Me.txtDateFrom, “yyyy/mm/dd”) & “#”)
End If

‘ 日付範囲(To)
If Not IsNull(Me.txtDateTo) Then
‘ 終了日の場合は時刻を23:59:59にするか、< #翌日# とするのが定石 Dim nextDay As Date nextDay = DateAdd("d", 1, Me.txtDateTo) conditions = AddCondition(conditions, "CreatedDate < #" & Format(nextDay, "yyyy/mm/dd") & "#") End If ' コンボボックス(完全一致) If Not IsNull(Me.cmbStatus) And Me.cmbStatus <> 0 Then
conditions = AddCondition(conditions, “StatusID = ” & Me.cmbStatus)
End If

BuildWhereClause = conditions
End Function

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

3. チーフアーキテクトが警鐘を鳴らす「罠」と最適化戦略

上記のコードは美しく堅牢に見えるが、Access特有のダークマター(暗黒物質)を理解していないと、現場で必ず破綻する。以下の3点を心に刻んでほしい。

① フォームの肥大化(Bloat)とメモリリーク

VBAで `Me.RecordSource = …` を頻繁に実行すると、Accessの内部エンジン(JET/ACE)は、裏で一時的なクエリ定義の作成と破棄を繰り返す。これが原因で `.accdb` ファイルが急速に肥大化し、ページの断片化を引き起こす。
対策: 検索フォームには必ず「クエリの最適化(Compact & Repairの自動化検討)」を意識させるとともに、複雑なJOINを含む場合は、ベースとなるパラメータ付きQueryDefをあらかじめ `.accdb` 内に作成しておき、VBAからは `Parameters` コレクション経由で値を渡す設計に昇華させるべきだ。

② `Application.Echo` と `Painting` の呪縛

大規模データを扱う検索画面で最もユーザーイライラを誘発するのが「画面のちらつき」と「描画遅延」である。
コード内にある `Application.Echo False` と `Me.Painting = False` はセットで使わなければ意味がない。

  • `Application.Echo False`: Accessウィンドウ全体の再描画を停止。
  • `Me.Painting = False`: 対象フォーム個別の描画を停止。

これを怠ると、RecordSourceが書き換わる瞬間にコントロールが1つずつ再描画され、CPU使用率が跳ね上がり、描画のゴースト現象が発生する。

③ インデックスの効かないワイルドカード

`LIKE ‘キーワード’` という前方・後方一致検索は、データベースのB-Treeインデックスを完全に無効化する(フルテーブルスキャンが発生する)。
数万件程度ならAccessは瞬殺してくれるが、数十万件を超えた途端にフリーズする。
もし数百万件規模をAccessのフロントエンドで検索させるという狂気的な要件があるならば、VBA側でSQLを組み立てるのではなく、SQL Server等のRDBへODBC接続し、サーバーサイドでストアドプロシージャを実行する形にアーキテクチャを移行するべきだ。その際も、今回解説した `RecordSource` の動的書き換え(厳密にはパススルー・クエリの動的変更)の概念がそのまま武器になる。

4. 結言:レガシーをレガシーで終わらせないために

「Accessだから適当に書いても動く」という時代は終わった。
オブジェクトのライフサイクルを管理し、メモリを解放し、SQLインジェクションや型エラーを完璧に潰しこんだコードこそが、現場の業務を支える真のエンジニアリングである。

動的 `RecordSource` の制御は、Access VBAにおける数少ない「モダンな動的クエリ構築のロジック」を適用できる聖域だ。本稿のコードと知見をあなたのシステムに導入し、圧倒的なパフォーマンスと安定性を手に入れてほしい。

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