【実務・中級編】動的SQL生成時の「引用符(’)」エスケープ処理を自動化する汎用関数 – Access VBA解析バイブル

スポンサーリンク

Access VBAでの開発において、避けて通れないのが「動的SQLの生成」だ。画面上のテキストボックスに入力された値を条件に、`QueryDef`を書き換えたり、`CurrentDb.Execute`でSQLを直叩きしたりするアーキテクチャは、業務システムの王道と言える。

しかし、ここで開発者の手を止める、そして本番稼働後に必ずと言っていいほどシステムをクラッシュさせる「隠れた地雷」がある。それがシングルクォーテーション( `’` )のエスケープ処理だ。

ユーザーが検索条件や入力値に「O’Brien」「It’s」といった文字を入力した瞬間、SQLの構文は破壊され、あの悪名高い「実行時エラー ‘3075’: 文字列式の構文エラー」が容赦なく画面を突き破る。

今回は、この問題に対して場当たり的な`Replace`関数の乱用で逃げるのではなく、プロのアーキテクトとしてバグの起きない堅牢なエスケープ自動化関数の設計思想と、実務でそのまま使えるプロダクションコードを伝授する。

—

なぜ従来の「手動Replace」は非効率で危険なのか?

多くの初級〜中級プログラマは、SQLを組み立てる際に以下のようなコードを書く。

‘ 【アンチパターン】場当たり的なエスケープ
Dim sql As String
sql = “SELECT FROM T_Customer WHERE CustomerName = ‘” & Replace(Me.txtCustomerName.Text, “‘”, “””) & “‘”

一見、シングルクォーテーションを2つに重ねる(`”`)処理が入っているため、正しく動くように見える。しかし、このアプローチには致命的な欠陥がある。

1. 書き忘れの温床になる:
システム内のあちこちにSQL文が散在する場合、開発者が毎回`Replace`関数を書く必要があり、新規追加や改修時に1箇所でも書き忘れると、そこが脆弱性(およびバグの温床)となる。
2. NULL値への耐性がない:
`Me.txtCustomerName.Text` が `Null`(未入力)だった場合、`Replace`関数は実行時エラーを引き起こすか、意図しない挙動を示す。
3. 可読性の低下:
SQL文の文字列結合の中にエスケープ処理が埋め込まれることで、本来のビジネスロジック(何の条件で絞り込んでいるか)が見えにくくなる。

プロのエンジニアであれば、「SQLに値を埋め込む前処理は、専用のユーティリティ関数にすべて隠蔽する」べきだ。

—

堅牢なエスケープ自動化関数の設計要件

実務の現場で耐えうる「エスケープ自動化関数」には、以下の要件が絶対的に求められる。

  • 型安全(Type Safety): 文字列だけでなく、数値、日付、そして `Null` 値を適切に判別し、SQLの構文に合わせたリテラル形式(文字列なら `’` で囲む、数値ならそのまま、NULLなら `NULL` と出力する)に自動変換すること。
  • 二重エスケープの防止: すでにエスケープ済みの文字列が渡された場合に、過剰に置换されない配慮、あるいはシンプルに「生データを受け取って確実にエスケープする」単一責任の原則を貫くこと。
  • Nullおよび長さ0の文字列のハンドリング: Access特有の「長さ0の文字列(`””`)」と「`Null`」の挙動の違いを吸収すること。

—

【コピペ即戦力】プロダクションコード:`SqlValue` ユーティリティ

標準モジュール(例: `modSqlUtils`)に以下のコードを配置してほしい。
この関数は、あらゆるデータ型を受け取り、SQLの構文安全性を完全に担保した文字列を返す「門番」として機能する。

Option Compare Database
Option Explicit

‘ =================================================================
‘ 模範的SQLユーティリティモジュール
‘ 概要: 渡された値をSQLの条件句に安全に埋め込める形式に変換する
‘ =================================================================

”’

”’ 任意の値を安全なSQLリテラル表現に変換します。
”’

”’ 変換対象の値(Variant型で何でも受け取る) ”’ オプション: 強制的に型指定したい場合の列挙 (今回は簡略化のためVariant判定) ”’ SQL文にそのまま結合できる文字列
Public Function SqlValue(ByVal vValue As Variant) As String

‘ 1. Null、または長さ0の文字列(ZLS)のハンドリング
If IsNull(vValue) Then
SqlValue = “NULL”
Exit Function
End If

If VarType(vValue) = vbString Then
If vValue = “” Then
‘ 業務要件に応じて空文字を “” (空の文字列)にするか NULL にするか決定する
‘ ここでは空文字を許容する場合のクォート表現とする
SqlValue = “””
Exit Function
End If

‘ 2. 文字列型の場合:シングルクォーテーションをエスケープし、両端を ‘ で囲む
‘ 「’」を「”」に置換するのがAccess SQLの基本ルール
Dim escaped As String
escaped = Replace(CStr(vValue), “‘”, “””)

‘ さらに、SQLインジェクションや予期せぬ制御文字対策が必要な場合はここで処理を追加
SqlValue = “‘” & escaped & “‘”

ElseIf IsNumeric(vValue) Then
‘ 3. 数値型の場合:クォートで囲まない(型変換エラーを防ぐためCStrで数値化)
SqlValue = CStr(vValue)

ElseIf IsDate(vValue) Then
‘ 4. 日付型の場合:Access SQL標準の # で囲む
‘ ※Accessは米国式(MM/DD/YYYY)またはISO(YYYY-MM-DD)を好むためFormat関数で担保
SqlValue = “#” & Format$(vValue, “yyyy/mm/dd hh:nn:ss”) & “#”

ElseIf VarType(vValue) = vbBoolean Then
‘ 5. ブール型の場合:True/False または 0/-1
SqlValue = IIf(vValue, “True”, “False”)

Else
‘ その他、オブジェクト型などは強制的に文字列化してエスケープ
SqlValue = “‘” & Replace(CStr(vValue), “‘”, “””) & “‘”

End If

End Function

—

現場での実践:QueryDef / 動的SQLでの活用例

上記の `SqlValue` 関数を使うことで、実際のSQL構築コードは劇的にクリーンになり、メンテナンス性が飛躍的に向上する。

以下は、画面の検索条件(テキストボックス、コンボボックス、チェックボックス)から動的に `QueryDef` を書き換えて実行する実務コードのサンプルだ。

Public Sub ExecuteDynamicSearch()
Dim db As DAO.Database
Dim qdf As DAO.QueryDef
Dim sql As String
Dim baseSql As String

Set db = CurrentDb

On Error GoTo ErrorHandler

‘ ベースとなるSQLクエリ(またはクエリ定義名)
baseSql = “SELECT CustomerID, CustomerName, Phone, CreatedDate FROM T_Customer WHERE 1=1”

‘ 動的条件の構築(SqlValue関数を通すことでエスケープ&型安全が完全自動化される)

‘ 1. 文字列検索(例: 顧客名部分一致)
If Not IsNull(Me.txtSearchName.Value) And Me.txtSearchName.Value <> “” Then
‘ ワイルドカード()を付与しつつ、値自体のシングルクォーテーションは完全にエスケープ
Dim searchPattern As String
searchPattern = “” & Me.txtSearchName.Value & “”

‘ ※Access SQLのLIKE演算子では を使う。SqlValueで囲むと ‘%文字列%’ になってしまうため注意。
‘ パターン1: LIKE演算子用に独自処理を入れる場合
baseSql = baseSql & ” AND CustomerName LIKE ‘” & Replace(Replace(Me.txtSearchName.Value, “‘”, “””), “”, “[]”) & “‘”
End If

‘ 2. 数値検索(例: 顧客ID)
If Not IsNull(Me.txtSearchID.Value) Then
‘ SqlValueを通せば、数値なのでクォートなしで安全に結合される
baseSql = baseSql & ” AND CustomerID = ” & SqlValue(Me.txtSearchID.Value)
End If

‘ 3. 日付範囲検索(例: 登録日)
If Not IsNull(Me.txtDateFrom.Value) Then
‘ 日付型も SqlValue が #YYYY/MM/DD# に自動フォーマット
baseSql = baseSql & ” AND CreatedDate >= ” & SqlValue(Me.txtDateFrom.Value)
End If

‘ — QueryDef への適用 —
‘ あらかじめ用意しておいた一時クエリ(qryDynamicSearch)のSQLを書き換える
Set qdf = db.QueryDefs(“qryDynamicSearch”)
qdf.SQL = baseSql
qdf.Close

‘ フォームのレコードソースを更新、あるいはサブフォームを再クエリ
Me.subformResult.Form.RecordSource = “qryDynamicSearch”
Me.subformResult.Form.Requery

Exit Sub

ErrorHandler:
MsgBox “検索処理でエラーが発生しました。” & vbCrLf & _
“エラー番号: ” & Err.Number & vbCrLf & _
“エラー内容: ” & Err.Description, vbCritical, “システムエラー”
End Sub

—

アーキテクトからの助言:パフォーマンスとデータベース連携の注意点

1. クエリキャッシュ(QueryPlan)の汚染について
動的SQL(文字列結合によるSQL生成)を毎回 `CurrentDb.Execute` や `QueryDef.SQL` の書き換えで行う場合、Access(Jet/ACEエンジン)は実行のたびに新しい実行プランをコンパイルし直す。
もしこれが数千件・数万件のループ内で実行されるようなバッチ処理であれば、パフォーマンスが著しく低下する。画面の検索や一括処理といった「人間の操作に紐づく頻度」であれば全く問題ないが、超高速処理が求められる場合は、パラメータクエリ(`Parameters` コレクションの明示的定義)の利用を検討すべきだ。

2. バックエンド(SQL Server等)へのアップサイジングへの備え
将来的にAccessのバックエンドをSQL Server(MDE/ACCDE + ODBCリンクテーブル、またはADP/AANからAPI連携)に移行する場合、Access特有のワイルドカード “ や日付の `#` 区切りでつまづくことになる。
今回紹介した `SqlValue` のようなユーティリティ関数層を一枚挟んでおけば、将来的にODBC経由のパラメータクエリ(`ADODB.Command` 等)へ移行する際も、アプリケーション側のロジックを書き換えることなく、関数内部のSQL構文生成ロジックを差し替えるだけで対応可能になる。

—

まとめ

「たかがシングルクォーテーションのエスケープ」と侮るなかれ。この処理の甘さは、そのままシステムの信頼性の欠如に直結する。

場当たり的な `Replace` の直書きを今すぐ廃止し、型を意識した堅牢なユーティリティ関数をアーキテクチャの根底に組み込むこと。それこそが、保守性に優れ、予期せぬエラーで現場を慌てさせないプロフェッショナルなAccess開発の極意である。

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