【実務・中級編】QueryDefのSQLプロパティを「外部JSONファイル」で管理する:ソースコードとクエリの完全分離 – Access VBA解析バイブル

スポンサーリンク

QueryDefのSQLを「外部JSON」で完全分離せよ:Access VBAの保守性を極限まで高めるアーキテクチャ

開発現場でこんな絶望を味わったことはないか?

数千行に及ぶAccess VBAのコードベース。その中に泥臭くベタ書きされた、改行もままならない長大なSQL文。要件定義の変更でカラム名が一つ変わっただけで、VBAのエディタを開き、エスケープ文字に頭を悩ませながら文字列結合の地獄をデバッグする――。

プログラミングの基本原則「関心の分離」を、なぜかAccess開発になると忘れたかのように、VBAコードとSQLテキストを癒着させる悪習が後を絶たない。

今回は、Access VBAにおける`QueryDef`オブジェクトのSQLプロパティを外部の「JSONファイル」から動的に読み込ませ、ソースコードとクエリを完全に切り離すプロフェッショナルな設計手法を伝授する。

このアーキテクチャを導入すれば、SQLの変更にVBAのコンパイルは不要になる。非エンジニアでもJSONさえ書き換えればクエリをチューニングできる、極めて堅牢で拡張性の高いシステムが手に入るのだ。

—

なぜ「VBAへのSQLベタ書き」は悪なのか?

実務において、VBAコード内にSQLを直接記述するアプローチには致命的な欠陥がある。

1. 可読性の欠如: ダブルクォーテーションの嵐、`& _`による行継続の煩雑さ。これでは複雑な結合(JOIN)やサブクエリの意図を読み取るだけでエンジニアの脳のメモリが消費される。
2. 保守コストの爆発: クエリのチューニングやスキーマ変更のたびに、VBAのソースコードを修正し、テストを行わなければならない。これは「変更に弱いコード」の典型例だ。
3. バージョン管理の不可能性: バイナリであるAccess(.accdb)の内部にSQLが隠蔽されていると、Gitなどのバージョン管理システムで「どこがどう変わったか」の差分(Diff)を追うのが極めて困難になる。

外部ファイル化、特にテキストベースで構造化データを表現できるJSON形式での管理こそが、この泥沼から抜け出す唯一にして最善の解である。

—

全体設計:JSONとQueryDefの連携フロー

今回構築する仕組みの全体像はこうだ。

1. 外部ストレージ(JSON): クエリの論理名(キー)と、実際のSQL文字列をペアにして外部ファイル(例: `queries.json`)で保持する。
2. VBAランタイム: 実行時にFileSystemObject(FSO)等でJSONを読み込み、VBA標準のJSONパーサー(または正規表現等)でパッシュする。
3. QueryDefへの動的バインド: 取得したSQL文字列を、一時的あるいは永続的な`QueryDef`オブジェクトの`.SQL`プロパティに流し込み、実行またはレコードセットを取得する。

今回は、外部ライブラリへの依存を最小限にするため、VBA環境で最も手堅く実装できる「VBA-JSON(Tim Hall氏のライブラリ)」、もしくは自前でパース可能な堅牢なアプローチを前提とする。実務では `VBA-JSON` を標準装備として話を進める。

—

実装ステップ:プロダクションコード

ここからは、明日から現場に投入できる実用的なコードを公開する。中途半端なサンプルではない。エラーハンドリングとオブジェクトのライフサイクル管理(メモリリークの防止)を徹底したプロのコードだ。

1. 外部JSONファイルの定義 (`queries.json`)

アプリケーションケータイと同じフォルダ、または特定のコンフィグフォルダに配置する。

{
“GetActiveCustomers”: “SELECT CustomerID, CompanyName, Region FROM tbl_Customers WHERE Status = ‘Active’ ORDER BY CompanyName;”,
“GetSalesSummaryByPeriod”: “SELECT p.CategoryName, SUM(s.Amount) AS TotalSales FROM tbl_Sales s INNER JOIN tbl_Categories p ON s.CategoryID = p.CategoryID WHERE s.SalesDate BETWEEN @StartDate AND @EndDate GROUP BY p.CategoryName;”
}

2. JSON読み込みとQueryDef制御モジュール (`clsQueryManager.cls`)

まずは、外部JSONを安全にロードし、指定されたキーのSQLを現在のAccessデータベースの`QueryDef`に流し込むクラスモジュールを作成する。

‘ =================================================================
‘ クラスモジュール名: clsQueryManager
‘ 概要: 外部JSONからSQLを読み込み、QueryDefを動的に構築する
‘ =================================================================
Option Explicit

Private m_JsonPath As String
Private m_ParsedData As Object

‘ コンストラクタでJSONファイルのパスを決定
Private Sub Class_Initialize()
‘ データベースと同じ階層の “config/queries.json” を指定
m_JsonPath = CurrentProject.Path & “\config\queries.json”
Call LoadJson
End Sub

‘ 終了時のクリーンアップ
Private Sub Class_ Terminate()
Set m_ParsedData = Nothing
End Sub

‘ JSONファイルの読み込みとパース
Private Sub LoadJson()
Dim fso As Object
Dim ts As Object
Dim jsonText As String

Set fso = CreateObject(“Scripting.FileSystemObject”)

If Not fso.FileExists(m_JsonPath) Then
Err.Raise 9999, “clsQueryManager”, “外部SQL定義ファイルが見つかりません: ” & m_JsonPath
End If

‘ UTF-8での読み込みを想定(FSOのOpenTextFileはデフォルトでASCII/SJISの場合があるため注意)
Set ts = fso.OpenTextFile(m_JsonPath, 1, False)
jsonText = ts.ReadAll
ts.Close

Set ts = Nothing
Set fso = Nothing

‘ VBA-JSON (JsonConverter) を使用してDictionaryに変換
‘ ※事前に VBA-JSON ライブラリのインポートが必要です
On Error GoTo ParseError
Set m_ParsedData = JsonConverter.ParseJson(jsonText)
Exit Sub

ParseError:
Err.Raise 9998, “clsQueryManager”, “JSONのパースに失敗しました。構文を確認してください。 Error: ” & Err.Description
End Sub

‘ 指定したクエリ名に対応するSQLをQueryDefにセットし、オブジェクトを返す
Public Function GetQueryDef(ByVal queryName As String, Optional ByVal targetQueryDefName As String = “temp_DynamicQuery”) As DAO.QueryDef
Dim db As DAO.Database
Dim qdf As DAO.QueryDef
Dim sqlText As String

Set db = CurrentDb

‘ JSONから該当キーのSQLを取得
If Not m_ParsedData.Exists(queryName) Then
Err.Raise 9997, “clsQueryManager”, “指定されたクエリ名がJSONに存在しません: ” & queryName
End If

sqlText = m_ParsedData(queryName)

‘ 既存のQueryDefが存在するかチェックし、なければ作成、あれば上書き
On Error Resume Next
Set qdf = db.QueryDefs(targetQueryDefName)
On Error GoTo 0

If qdf Is Nothing Then
Set qdf = db.CreateQueryDef(targetQueryDefName, sqlText)
Else
qdf.SQL = sqlText
End If

Set GetQueryDef = qdf

Set qdf = Nothing
Set db = Nothing
End Function

3. 実践:フォームや標準モジュールからの呼び出し

上記のクラスを使って、実際にデータを取得する標準モジュールのコードを見てみよう。パラメータクエリの安全なバインド方法も併せて示す。

‘ =================================================================
‘ 標準モジュール: mdlMain
‘ =================================================================
Option Explicit

Public Sub ExecuteDynamicQuerySample()
Dim qMgr As clsQueryManager
Dim qdf As DAO.QueryDef
Dim rst As DAO.Recordset

‘ クラスのインスタンス化
Set qMgr = New clsQueryManager

On Error GoTo ErrorHandler

‘ 1. パラメータを含まないシンプルクエリの実行
Set qdf = qMgr.GetQueryDef(“GetActiveCustomers”, “qry_Dynamic_ActiveCustomers”)
Set rst = qdf.OpenRecordset()

MsgBox “アクティブ顧客数: ” & rst.RecordCount & “件”, vbInformation, “結果”

rst.Close

‘ 2. パラメータクエリ(期間指定)の実行
‘ ※QueryDefを使うことで、SQLインジェクションや型ミスマッチを防ぎながら安全にパラメータを渡せる
Set qdf = qMgr.GetQueryDef(“GetSalesSummaryByPeriod”, “qry_Dynamic_SalesSummary”)

qdf.Parameters(“@StartDate”) = #2023-01-01#
qdf.Parameters(“@EndDate”) = #2023-12-31#

Set rst = qdf.OpenRecordset()

‘ レコードの処理ループ(省略)
Do Until rst.EOF
‘ Debug.Print rst.Fields(“CategoryName”).Value & “: ” & rst.Fields(“TotalSales”).Value
rst.MoveNext
Loop

CleanUp:
‘ オブジェクトの解放(メモリリークの完全防止)
If Not rst Is Nothing Then rst.Close: Set rst = Nothing
If Not qdf Is Nothing Then Set qdf = Nothing
If Not qMgr Is Nothing Then Set qMgr = Nothing
Exit Sub

ErrorHandler:
MsgBox “エラーが発生しました: ” & Err.Description, vbCritical, “致命的エラー”
Resume CleanUp
End Sub

—

チーフアーキテクトが教える「現場の罠」と回避策

このアーキテクチャを本番環境に導入する際、素人が踏みがちな地雷がいくつか存在する。プロとして知っておくべき注意点を共有しよう。

1. 文字コード(UTF-8)の呪縛

FileSystemObjectの `.OpenTextFile` メソッドは、デフォルトではシステムロケール(日本語版WindowsならShift-JIS)でファイルを読み込もうとする。
JSONファイルをUTF-8(BOM付き)で保存するか、FSOではなく `ADODB.Stream` オブジェクトを使用して明示的に `charset = “UTF-8″` を指定して読み込む実装にすべきである。多言語対応や特殊文字を含む環境では、ここを手抜くと文字化けの魔物に取り憑かれる。

2. DAOとADOの混同によるパフォーマンス低下

Access VBAでは、ローカルのテーブルや`QueryDef`を操作する場合、迷わず DAO (Data Access Objects) を選択しろ。ADOはネットワーク経由のRDBMS(SQL Server等)には強いが、Jet/ACEエンジンを直叩きするAccessのローカル環境においてはDAOの方が圧倒的にオーバヘッドが少なく、メモリ効率が良い。今回のコードで `DAO.QueryDef` を採用しているのはそのためだ。

3. 一時QueryDefの名前衝突(コンカレントアクセス)

マルチユーザー環境や、非同期に近いタイマー処理などで同じ名前のテンポラリQueryDef(例: `temp_DynamicQuery`)を同時に書き換えようとすると、ロック競合や予期せぬ挙動を引き起こす。
本番稼働するシステムでは、セッションIDやグローバル一意識別子(GUID)、あるいはユーザー名等を付与して、QueryDef名が一意になるような工夫(動的生成)を取り入れるのが、シニアエンジニアの流儀である。

—

結論:プロの誇りを持ったコードを書け

「Accessだから、VBAだから、これくらい雑でもいいか」という妥協が、数ヶ月後の改修時に自分(あるいは後任のエンジニア)の首を絞めることになる。

SQLを外部JSONファイルで完全に切り離すこの設計手法は、単なる「テクニック」ではない。「メンテナンス性」「拡張性」「テスタビリティ」を担保するためのエンジニアの良心である。

今すぐ既存のベタ書きSQLをコードから排除し、外部JSON管理へのリファクタリングを始めろ。あなたの作るAccessアプリケーションは、もっと美しく、もっと強靭になれるはずだ。

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