【実務・中級編】Accessの「クエリデザインビュー」と「VBA生成」を共存させるハイブリッド開発手法 – Access VBA解析バイブル

スポンサーリンク

Access VBAを掌握する極限の知見:クエリデザインビューと動的SQLのハイブリッド共存術

開発現場でよく見かける二大宗教戦争がある。「すべてのSQLをVBA内で動的構築すべし派」と「いや、すべてのロジックをデザインビューのクエリに閉じ込めるべし派」だ。

結論から言こう。どちらも実務の現場においては片翼飛行であり、破滅への片道切符だ。

VBA内で数千文字に及ぶ複雑なJOINとサブクエリを組み立てているコードを見たことがあるか? メンテナンス性最悪、インデントもクソもない文字列連結、少しの仕様変更で爆発するバグ。かといって、無限に増殖する「〇〇用クエリ1」「〇〇用クエリ_最終確定」といったデザインビューの残骸群。これもまた、属人化を生む悪夢のスパゲッティ・データベースの典型例だ。

真に堅牢で、保守性が高く、なおかつ変化に強いシステムを構築したいなら、「構造(JOIN)はデザインビューに委ね、可変値(WHERE句)はVBAで動的注入する」というハイブリッド設計を採用せよ。

今回は、Accessのデータベースエンジン(Jet / ACE)の挙動を知り尽くしたアーキテクトが、このハイブリッド手法の極意とプロダクションコードを伝授する。

—

1. なぜ「全VBA構築」でも「全デザインビュー」でも破綻するのか?

全VBA構築の罠

SQL文をVBAのString型変数で組み立てる手法は、一見すると「ファイルが一つにまとまっていてスマート」に見える。だが、実務では以下の致命的なコストを払うことになる。

  • コンパイル時チェックの不在: SQLの構文ミスやフィールド名のタイポは、実際にそのコードが走るまで検知できない。
  • デバッグの地獄: 実行時エラーが出た際、巨大なSQL文字列をイミディエイトウィンドウに吐き出し、それをコピーしてクエリデザイナに貼り付けて…という泥臭い作業に毎度追われる。

全デザインビューの罠

GUIでクエリを作るのは楽だ。しかし、「ユーザーが画面で選んだ条件(期間、担当者、フラグなど)によってWHERE句を無限に変えたい」という要件に直面した途端、デザインビューだけでは太刀打ちできなくなる。結果、フォームのコントロール値を直接参照するような悪臭を放つ(Smellな)クエリが乱立し、挙動の追跡が不可能になる。

—

2. ハイブリッド設計の核心:QueryDefのパラメータ動的書き換え

この問題をスマートに解決するのが、「ベースとなるSELECT/JOINクエリをあらかじめデザインビュー(または保存されたQueryDef)で作っておき、VBAからその`SQL`プロパティ、あるいは`Parameters`コレクションを操作する」というアプローチだ。

ここでAccess開発者が陥りがちなアンチパターンを指摘しておこう。
「クエリのSQL文全体をVBAで丸ごと書き換える」のはまだマシだが、それだと結局デザインビューの恩恵(ビジュアルな結合関係の確認)を捨てていることになる。

我々がやるべきは、「保存されたクエリの末尾に、VBAで構築したWHERE句を安全にマージする」、あるいは「QueryDefオブジェクトにパラメータをバインドする」ことだ。

—

3. 実装:プロダクションコード例

現場で即座に使える、極めて堅牢な実装パターンを提示する。
ここでは、ベースとなる複雑な結合クエリ `qry_Sales_Base` がすでにデザインビューで存在している前提とする。

【前提となるベースクエリ:`qry_Sales_Base` のイメージ】

SELECT T_Sales.SalesID, T_Sales.SalesDate, M_Customer.CustomerName, M_Item.ItemName, T_Sales.Quantity, T_Sales.Amount
FROM (T_Sales
INNER JOIN M_Customer ON T_Sales.CustomerID = M_Customer.CustomerID)
INNER JOIN M_Item ON T_Sales.ItemID = M_Item.ItemID;

この複雑なJOIN構造(変更したくない部分)はデザインビューに封印する。

【VBAモジュール:動的WHERE句の注入と実行】

以下のプロシージャは、画面上の検索条件フォームから値を受け取り、安全かつ動的にクエリを制御してレコードセットを取得するテンプレートだ。

Option Compare Database
Option Explicit

Public Sub ExecuteDynamicQueryExample()
Dim db As DAO.Database
Dim qdf As DAO.QueryDef
Dim rst As DAO.Recordset
Dim strSQL As String
Dim strWhere As String

‘ エラーハンドリングの鉄則
On Error GoTo ErrorHandler

Set db = CurrentDb

‘ 1. ベースとなるクエリ定義を取得
‘ ※デザインビューで作成・保存された結合クエリ名を指定
Set qdf = db.QueryDefs(“qry_Sales_Base”)

‘ 2. 基本のSQLを取得(JOIN部分が完全に保全されている)
strSQL = qdf.SQL

‘ — ここから動的WHERE句の構築 —
strWhere = ” WHERE 1=1″ ‘ 条件連結を容易にするためのイディオム

‘ 条件1: 日付範囲指定(フォームから値を取得する想定)
‘ ※SQLインジェクションや型ミスの防止のため、厳密な型評価を行う
If IsDate(Forms!F_Search!txtDateFrom) Then
strWhere = strWhere & ” AND T_Sales.SalesDate >= #” & Format(Forms!F_Search!txtDateFrom, “yyyy/mm/dd”) & “#”
End If

If IsDate(Forms!F_Search!txtDateTo) Then
strWhere = strWhere & ” AND T_Sales.SalesDate <= #" & Format(Forms!F_Search!txtDateTo, "yyyy/mm/dd") & "#" End If ' 条件2: 顧客ID指定(数値型) If Not IsNull(Forms!F_Search!cmbCustomer) Then strWhere = strWhere & " AND T_Sales.CustomerID = " & CLng(Forms!F_Search!cmbCustomer) End If ' 条件3: 商品名部分一致(文字列型・SQLインジェクション対策のエスケープ処理含む) If Trim(Nz(Forms!F_Search!txtItemName, "")) <> “” Then
Dim safeKeyword As String
safeKeyword = Replace(Forms!F_Search!txtItemName, “‘”, “””) ‘ シングルクォーテーションのエスケープ
strWhere = strWhere & ” AND M_Item.ItemName LIKE ‘” & safeKeyword & “‘”
End If

‘ — 構築完了 —

‘ 3. 一時的な実行用クエリ(または一時テーブル用)としてSQLを統合
‘ 実際には、既存の作業用クエリ「qry_Sales_Working」のSQLを書き換える手法が安全
Dim qdfWork As DAO.QueryDef
Set qdfWork = db.QueryDefs(“qry_Sales_Working”)

‘ ORDER BY句が必要な場合はここで追加する
qdfWork.SQL = strSQL & strWhere & ” ORDER BY T_Sales.SalesDate DESC;”

‘ 4. レコードセットのオープン(パフォーマンス最適化のため dbOpenSnapshot を推奨)
Set rst = qdfWork.OpenRecordset(dbOpenSnapshot)

‘ 【データ処理ループ】
If rst.RecordCount > 0 Then
rst.MoveFirst
Do Until rst.EOF
‘ TODO: 業務ロジック(エクスポート、画面への反映など)をここに記述
‘ 例: Debug.Print rst!CustomerName & ” – ” & rst!Amount
rst.MoveNext
Loop
MsgBox rst.RecordCount & ” 件のデータを処理しました。”, vbInformation
Else
MsgBox “条件に一致するデータはありません。”, vbExclamation
End If

CleanUp:
‘ 5. オブジェクトの明示的な解放(メモリリーク・ロック防止の極意)
If Not rst Is Nothing Then rst.Close: Set rst = Nothing
If Not qdfWork Is Nothing Then Set qdfWork = Nothing
If Not qdf Is Nothing Then Set qdf = Nothing
If Not db Is Nothing Then Set db = Nothing
Exit Sub

ErrorHandler:
MsgBox “予期せぬエラーが発生しました。” & vbCrLf & _
“Error: ” & Err.Number & ” – ” & Err.Description, vbCritical
Resume CleanUp
End Sub

—

者として、このコードのキモをいくつか解説しておこう。

1. `WHERE 1=1` のイディオム:
動的にAND条件を追加していく際、「最初の条件かどうか」を判定するIF文を書くのはコードが汚れる元だ。最初に `WHERE 1=1` を仕込んでおけば、すべての条件を `AND ~` で一律に結合できる。これはVBAに限らず、プロフェッショナルなSQL構築の定石である。
2. 文字列のエスケープ処理:
`Replace(val, “‘”, “””)` を忘れているコードは、ユーザーが名前に「O’Brien」のような入力(シングルクォーテーション)をした瞬間に構文エラーでクラッシュする。プロダクションコードを名乗るなら、この一手間を絶対に省いてはならない。
3. 適切なレコードセットタイプ(`dbOpenSnapshot`):
読み取り専用でデータを処理する場合、デフォルトの `dbOpenDynaset` を使うのはリソースの無駄遣いだ。スナップショットを使用することで、Jet/ACEエンジンのロックオーバヘッドを軽減し、爆発的なパフォーマンス向上が望める。
4. 確実なオブジェクト解放:
Access VBAにおいて、`DAO.Database` や `Recordset`、`QueryDef` をローカル変数で宣言して解放し忘れると、内部キャッシュの肥大化やファイルロックの遠因となる。必ず `Set xxx = Nothing` を通るパスを保証しろ。

—

4. チーム開発・ファイル連携における注意点

このハイブリッド手法を実際の現場(特に複数人での開発や、バックエンド/フロントエンド分離構成)に導入する際、以下の罠に注意してほしい。

  • バックエンド(データ)側のクエリは触らせるな:

ACCDE運用(コンパイル済み配布)を行っている場合、フロントエンド側からバックエンド側の `QueryDef` を動的に書き換えることはできない(バックエンド側は読み取り専用になるため)。したがって、動的にSQLを書き換える対象のクエリは必ず「フロントエンド側」に作成・保存すること。

  • ローカル一時クエリの活用:

複数のユーザーが同時にこのVBAを実行する場合、共有のクエリ定義(例: `qry_Sales_Working`)を書き換えると、競合(レースコンディション)が発生し、他のユーザーの処理を破壊する。
本格的なマルチユーザー環境では、クエリのSQLを動的に書き換えるのではなく、DAOの `CreateQueryDef` で一時的な名前(あるいは名前なしのQueryDef)を作成して実行するか、あるいはADOのCommandオブジェクト(パラメータクエリ)を利用する設計に昇華させるべきだ。

—

5. 結びにかえて

「デザインビューの視覚的優位性」と「VBAの動的柔軟性」。
この二つを対立するものとして捉えるのは、もう終わりにするべきだ。

結合の美しさはAccessのオプティマイザが最も得意とするデザインビューに任せ、条件のゆらぎは洗練されたVBAでスマートに制御する。この境界線を正確に引き渡すアーキテクチャこそが、あなたの作るAccessシステムを「すぐに壊れるおもちゃ」から「現場を支える堅牢なインフラ」へと変貌させる唯一の道である。

プロの手腕を見せつけろ。綺麗なコードと圧倒的な安定性は、いつだって正しい設計の頭脳からしか生まれない。

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