【テクニカル・上級編】動的SQL生成時の「クォーテーション地獄」を回避するスマートな文字列構築法 – Access VBA解析バイブル

スポンサーリンク

Access VBAを掌握する極限の知見:動的SQL生成における「クォーテーション地獄」の完全駆逐

レガシーシステムの暗部において、Access VBAエンジニアの精神を最も蝕むもの。それは、動的SQLを組み立てる際に発生する「クォーテーション地獄」である。

日付の書式、文字列内のシングルクォーテーション (`’`) エスケープ、そしてNULL値の混入。これらが複雑に絡み合った文字列連結式は、コードの可読性を完全に破壊し、SQLインジェクションの温床となり、メンテナンス時には開発者を絶望の淵へと叩き込む。

「`”” & と `'” & … & “‘` の嵐」から脱却し、プロフェッショナルなアーキテクチャを手に入れるための極限の知見をここに公開する。

—

なぜ「文字列連結」によるSQL生成は悪なのか

初学者はSQLを以下のように組み立てがちだ。

‘ 悪夢のようなレガシーコード
Dim strSQL As String
strSQL = “SELECT FROM T_Order WHERE CustomerName = ‘” & Me.txtCustomer & “‘ AND OrderDate >= #” & Me.txtDate & “#”

このアプローチには、致命的な欠陥が三つ存在する。

1. 構文エラーの常態化: ユーザーが「O’Brien」のような名前にシングルクォーテーションを含んでいた場合、SQLは即座に構文エラーを起こしてクラッシュする。
2. 型安全性(Type Safety)の完全な欠如: 日付型のロケール問題(US形式 `MM/DD/YYYY` かどうか)や、NULLの扱いでバグが頻発する。
3. パフォーマンスとメモリの非効率: VBAの文字列型(BSTR)はイミュータブル(不変)であり、安易な連結はメモリのヒープ領域に無駄な断片化を引き起こす。

我々が目指すべきは、SQL文の構築を「文字列のパズル」から「型安全なパラメータバインディング(またはそれに準ずる厳格なサニタイズ)」へと昇華させることだ。

—

決定解:型安全なSQLビルダー・ヘルパー関数の実装

ADOの `Command` オブジェクトと `Parameters` コレクションを使用するのが本来のセオリーではあるが、DAOの `QueryDef` を動的に書き換える際や、複雑なLIKE検索などを安全にインライン構築する必要がある現場では、「入力を型ごとに厳格にエスケープ・フォーマットする専用ヘルパー関数」をグローバルモジュールとして常備するのが最も実用的かつ堅牢である。

以下に、実務の最前線で耐えうる極限まで最適化されたヘルパーモジュール `SqlUtils` の実装を示す。

‘ ==============================================================================
‘ モジュール名: M_SqlUtils
‘ 概要: 動的SQL生成におけるクォーテーション地獄を根絶する型安全ヘルパー
‘ ==============================================================================
Option Explicit

‘ ——————————————————————————
‘ 狙い: 文字列型を安全にSQLリテラルに変換する(シングルクォーテーションの二重化)
‘ ——————————————————————————
Public Function SqlStr(ByVal Value As Variant) As String
If IsNull(Value) Then
SqlStr = “NULL”
Else
‘ SQLインジェクションおよび構文エラーを防ぐため ‘ を ” に置換
SqlStr = “‘” & Replace(CStr(Value), “‘”, “””) & “‘”
End If
End Function

‘ ——————————————————————————
‘ 狙い: 日付型をAccess/Jetエンジンが確実に理解するUS標準形式(#YYYY-MM-DD#)に変換
‘ ——————————————————————————
Public Function SqlDate(ByVal Value As Variant) As String
If IsDate(Value) Then
‘ ロケール依存を排除するため ISO-8601 類似の書式に強制変換
SqlDate = “#” & Format$(CDate(Value), “yyyy-mm-dd hh:nn:ss”) & “#”
Else
SqlDate = “NULL”
End If
End Function

‘ ——————————————————————————
‘ 狙い: 数値型を安全にフォーマットし、型ミスマッチやSQLインジェクションを阻止
‘ ——————————————————————————
Public Function SqlNum(ByVal Value As Variant) As String
If IsNumeric(Value) And Not IsNull(Value) Then
‘ カンマや通貨記号を除外し、厳密な数値文字列へ
SqlNum = CStr(Val(CStr(Value)))
Else
SqlNum = “0” ‘ または用途に応じて “NULL”
End If
End Function

‘ ——————————————————————————
‘ 狙い: 部分一致検索(LIKE)用のワイルドカードを安全に付与する
‘ ——————————————————————————
Public Function SqlLike(ByVal Value As Variant, Optional ByVal MatchType As Integer = 0) As String
If IsNull(Value) Or CStr(Value) = “” Then
SqlLike = “””
Exit Function
End If

Dim escaped As String
escaped = Replace(CStr(Value), “‘”, “””)
‘ 特殊文字の簡易エスケープ(必要に応じて [%] [[] 等も処理対象とする)
escaped = Replace(escaped, “_”, “[_]”)
escaped = Replace(escaped, “%”, “[%]”)

Select Case MatchType
Case 1: SqlLike = “‘%” & escaped & “‘” : ‘ 前方一致
Case 2: SqlLike = “‘” & escaped & “%'” : ‘ 後方一致
Case Else: SqlLike = “‘%” & escaped & “%'” : ‘ 完全・中間一致
End Select
End Function

—

現場での実践:QueryDefと組み合わせたスマートな実装

作成したヘルパー関数を実際の業務ロジック(`QueryDef` を利用した動的クエリの生成と実行)に適用する。ここでは、オブジェクトのライフサイクルを厳密に管理し、メモリリークやリソースの解放漏れを防ぐプロフェッショナルなパターンを示す。

Public Sub ExecuteDynamicQuerySample()
Dim db As DAO.Database
Dim qdf As DAO.QueryDef
Dim strSQL As String
Dim rs As DAO.Recordset

‘ 外部入力の模擬(画面コントロール値など)
Dim inputCustomer As Variant: inputCustomer = “O’Brien & Co.”
Dim inputDate As Variant: inputDate = #1/15/2026#
Dim inputLimit As Variant: inputLimit = 100

On Error GoTo ErrorHandler

‘ カレントデータベースの参照を取得(明示的な変数保持によりスコープを制御)
Set db = CurrentDb()

‘ 1. ヘルパー関数群を駆使した、圧倒的にクリーンなSQL構築
strSQL = “SELECT TOP ” & SqlNum(inputLimit) & ” ” & _
“FROM T_Orders ” & _
“WHERE CustomerName = ” & SqlStr(inputCustomer) & ” ” & _
” AND OrderDate >= ” & SqlDate(inputDate) & ” ” & _
” AND Memo LIKE ” & SqlLike(“重要”, 1)

‘ デバッグ時に完成したSQLを確認可能
Debug.Print strSQL

‘ 2. 一時的なQueryDefオブジェクトの動的生成と再利用の最適化
Const QRY_NAME As String = “tmp_DynamicQuery”

‘ 既存の同名クエリが存在する場合は削除(エラーハンドリングまたは存在チェック)
On Error Resume Next
db.QueryDefs.Delete QRY_NAME
On Error GoTo ErrorHandler

‘ QueryDefの新規作成
Set qdf = db.CreateQueryDef(QRY_NAME, strSQL)

‘ 3. レコードセットの取得と処理
Set rs = qdf.OpenRecordset(dbOpenSnapshot)

Do Until rs.EOF
‘ — 業務処理 —
‘ Debug.Print rs.Fields(“OrderID”).Value
rs.MoveNext
Loop

CleanUp:
‘ ————————————————————————–
‘ オブジェクトの明示的解放(メモリ最適化の極意)
‘ ————————————————————————–
If Not rs Is Nothing Then
rs.Close
Set rs = Nothing
End If

If Not qdf Is Nothing Then
‘ 一時クエリとして破棄する場合
db.QueryDefs.Delete QRY_NAME
Set qdf = Nothing
End If

Set db = Nothing
Exit Sub

ErrorHandler:
MsgBox “エラー発生: ” & Err.Description, vbCritical
Resume CleanUp
End Sub

—

チーフアーキテクトからの提言

レガシーなAccessシステムにおいて、「動的SQLの組み立て」はシステムの寿命を決定するファクターである。場当たり的な文字列連結を放置することは、技術的負債に自ら利息を払い続ける行為に他ならない。

今回紹介した `SqlStr`、`SqlDate`、`SqlNum` といった型変換ラッパーを標準モジュールに組み込み、すべてのSQL構築のインターフェースを統一せよ。それだけで、クォーテーションに起因するバグはゼロになり、コードの品格は世界最高峰のエンジニアリング水準へと到達する。

妥協なきコードベースこそが、長寿命システムの唯一の盾である。

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