プロの流儀: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から分離することは、単なるテクニックではない。それは、アプリケーションのロジックとデータの定義を明確に分ける「規律」である。
この設計を導入することで、あなたの作成するツールは「マクロ」の域を超え、プロフェッショナルな「ソフトウェア」へと昇華する。コードの美しさは、そのままシステムの堅牢さに直結することを忘れないでほしい。
