【実務・中級編】QueryDefのSQLを「外部テキストファイル」で管理する:ソースコードとクエリの分離による保守性向上 – Access VBA解析バイブル

スポンサーリンク

プロの流儀:VBAコードからSQLを追放せよ。QueryDefと外部ファイルの完全分離結界

Access VBAで大規模な業務システムを構築する際、初心者が必ず陥る「地獄」がある。それが、VBAコード内に延々と続く `strSQL = strSQL & ” … “` の泥沼だ。

可読性は最悪、ダブルクォーテーションのエスケープで消耗し、SQLを一行修正するだけでVBAのコンパイルが必要になる。挙句の果てに、実行時まで構文エラーに気づけない。

現場を統括するアーキテクトとして断言する。「SQLはVBAの中に書くものではない。外に置くものだ。」

今回は、SQLを外部テキストファイル(.sql)で管理し、`DAO.QueryDef` を介して動的に実行する、堅牢かつ保守性の極致を行く設計手法を伝授する。

—

1. なぜ「外部ファイル化」が不可欠なのか

理由は単純、「関心の分離(Separation of Concerns)」だ。

1. メンテナンス性: SQLの修正にVBAの修正(デプロイ)が不要になる。メモ帳やVS CodeでSQLを開き、実行計画を確認しながら修正して上書き保存するだけだ。
2. 可読性: SQLのインデントやコメントがそのまま保持される。VBAの連結演算子(&)に邪魔されることなく、純粋なクエリとして管理できる。
3. 再利用性とテスト: 外部ファイルなら、そのままSQL Server Management StudioやAccessのクエリデザインにコピペして単体テストができる。
4. セキュリティ: パラメータークエリを強制することで、SQLインジェクションの脆弱性を物理的に排除する。

—

2. 実装の全体像:QueryDefのライフサイクルを支配する

外部ファイルからSQLを読み込み、パラメーターを注入して実行するまでのフローを以下に定義する。

1. SQLファイルのロード: `FileSystemObject` を使い、指定したパスのテキストを読み込む。
2. QueryDefの生成: データベース内に一時的な(あるいは名前付きの)`QueryDef` を作成する。
3. パラメーターのバインド: SQL内の `[prmCity]` といったプレースホルダーに、VBA側から値を安全に流し込む。
4. 実行/レコードセット取得: アクションクエリなら `Execute`、選択クエリなら `OpenRecordset`。

—

3. 実戦プロダクションコード

以下のコードは、実務でそのまま利用可能なレベルまで抽象化・堅牢化したものだ。

3.1 SQLファイルの準備

例えば、`C:\App\Sql\GetOrderSummary.sql` として保存する。

— 外部ファイル側:コメントもインデントも自由自在
PARAMETERS [prmStartDate] DateTime, [prmMinAmount] Currency;
SELECT
CustomerName,
Sum(OrderAmount) AS TotalAmount
FROM
tblOrders
WHERE
OrderDate >= [prmStartDate]
GROUP BY
CustomerName
HAVING
Sum(OrderAmount) >= [prmMinAmount];

3.2 VBA:QueryManagerクラス/モジュール

Option Compare Database
Option Explicit

‘ @description SQLファイルを読み込み、パラメーターを適用してRecordsetを返す
‘ @param sqlFileName SQLファイル名(拡張子込)
‘ @param params 投入するパラメーター(Dictionaryオブジェクトを使用)
‘ @return DAO.Recordset
Public Function ExecuteSecureQuery(ByVal sqlFileName As String, _
Optional ByVal params As Object = Nothing) As DAO.Recordset
On Error GoTo Err_Handler

Dim db As DAO.Database
Dim qdf As DAO.QueryDef
Dim sqlText As String
Dim sqlPath As String
Dim key As Variant

Set db = CurrentDb

‘ 1. SQLファイルの読み込み
‘ ※パスの管理は定数や設定テーブルで行うのが定石
sqlPath = CurrentProject.Path & “\Sql\” & sqlFileName
sqlText = LoadSqlText(sqlPath)

‘ 2. 一時的なQueryDefの作成(名前を空にすると保存されない)
Set qdf = db.CreateQueryDef(“”, sqlText)

‘ 3. パラメーターの注入
If Not params Is Nothing Then
For Each key In params.Keys
‘ SQL内のパラメータ名とDictionaryのキーを一致させる
qdf.Parameters(key).Value = params(key)
Next key
End If

‘ 4. 実行(選択クエリの場合)
‘ アクションクエリの場合は qdf.Execute dbFailOnError を使用
Set ExecuteSecureQuery = qdf.OpenRecordset(dbOpenSnapshot)

Clean_Exit:
Set qdf = Nothing
Exit Function

Err_Handler:
MsgBox “SQL実行エラー [” & sqlFileName & “]: ” & Err.Description, vbCritical
Resume Clean_Exit
End Function

‘ @description ファイルからSQL文字列を抽出(FSOを使用)
Private Function LoadSqlText(ByVal fullPath As String) As String
Dim fso As Object ‘ Scripting.FileSystemObject
Dim ts As Object ‘ Scripting.TextStream

Set fso = CreateObject(“Scripting.FileSystemObject”)

If Not fso.FileExists(fullPath) Then
Err.Raise 53, , “SQLファイルが見つかりません: ” & fullPath
End If

‘ UTF-8やShift-JISの考慮が必要な場合はADODB.Streamを検討
Set ts = fso.OpenTextFile(fullPath, 1) ‘ 1 = ForReading
LoadSqlText = ts.ReadAll
ts.Close

Set ts = Nothing
Set fso = Nothing
End Function

3.3 呼び出し側のコード

Public Sub RunBusinessLogic()
Dim params As Object
Set params = CreateObject(“Scripting.Dictionary”)

‘ パラメーターの設定
params.Add “[prmStartDate]”, #1/1/2023#
params.Add “[prmMinAmount]”, 50000

Dim rs As DAO.Recordset
Set rs = ExecuteSecureQuery(“GetOrderSummary.sql”, params)

‘ 結果の処理
Do While Not rs.EOF
Debug.Print rs!CustomerName & “: ” & rs!TotalAmount
rs.MoveNext
Loop

rs.Close
Set rs = Nothing
End Sub

—

4. 現場のチーフアーキテクトが教える「極意」

1. 文字コードの罠に注意せよ

`FileSystemObject` はデフォルトでShift-JIS(ANSI)を想定する。VS CodeなどでSQLを作成し、UTF-8で保存すると文字化けが発生し、SQL構文エラー(予約語の欠落など)を招く。
現場では、「SQLファイルは常にBOM付きUTF-16またはShift-JISで統一する」か、読み込み部を `ADODB.Stream` に差し替えてエンコーディングを明示的に指定するのがプロの仕事だ。

2. QueryDefの「一時性」を活かせ

`db.CreateQueryDef(“”)` のように、第一引数に空文字を渡すと、データベースの「クエリ」一覧に保存されない一時的なオブジェクトが生成される。これにより、システムテーブル(MSysObjects)を汚さず、名前の衝突を気にせずに動的クエリを生成できる。

3. パラメーターの「型」を明示せよ

SQLファイルの先頭に `PARAMETERS [prmName] DataType;` を記述するのは面倒に思えるかもしれない。しかし、これを書くことでAccessのクエリエンジンが最適化を行い、暗黙の型変換によるパフォーマンス劣化を防ぐことができる。また、日付形式の解釈ミス(US形式かJP形式か)という、Access特有の呪いから解放される。

—

結論

SQLをVBAから分離することは、単なるテクニックではない。それは、アプリケーションのロジックとデータの定義を明確に分ける「規律」である。

この設計を導入することで、あなたの作成するツールは「マクロ」の域を超え、プロフェッショナルな「ソフトウェア」へと昇華する。コードの美しさは、そのままシステムの堅牢さに直結することを忘れないでほしい。

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