Access VBAを掌握する極限の知見
第1回:動的SQL生成時の「クォーテーション地獄」を完全に駆逐するスマート設計論
開発現場でAccess VBAを使い倒しているあなたなら、一度は絶望したことがあるはずだ。そう、「動的SQLの文字列連結」である。
画面上のテキストボックスに入力された条件を拾い、SQL文を組み立てる。文字列型ならシングルクォーテーション `’` で囲み、日付型ならシャープ `#` で囲み、数値型はそのままだ。ここに「入力値にシングルクォーテーションが含まれている場合のエスケープ処理」や「Null値の判定」が絡み合った瞬間、コードは一気に汚濁し、無数のバグを生む温床となる。
いわゆる「クォーテーション地獄」だ。
今回は、このAccess開発者永遠の呪縛を断ち切り、圧倒的な堅牢性と保守性を誇る動字SQL生成の極意を、チーフアーキテクトの視点から授けよう。
—
1. なぜ「その場のノリ」で書くSQL連結が地獄を招くのか
多くの開発者は、次のようなコードを平然と書いてしまう。
‘ 【アンチパターン】絶対に真似してはならない典型的な実装
Dim sql As String
sql = “SELECT FROM T_受注 WHERE 顧客名 = ‘” & Me.txt顧客名.Text & “‘ AND 受注日 >= #” & Me.txt開始日.Text & “#”
このコードの何がクソなのか。理由は3つある。
1. 型意識の欠如: すべての変数を無理やり文字列として扱っているため、データベースの型仕様(Jet/ACE SQLの厳格さ)に依存したエラーが起きやすい。
2. SQLインジェクション&構文エラーの耐性ゼロ: 顧客名にアポストロフィ(`O’Brien`など)が混入した瞬間、SQLの構文が破壊され、実行時エラー(実行時エラー 3075: 構文エラー)が爆誕する。
3. 保守性の死: 条件分岐が増えれば増えるほど `& ” AND ” &` の海に溺れ、どこにクォーテーションが足りないのかデバッグに何時間も溶かすことになる。
プロのエンジニアであれば、SQL文を「その都度手動で組み立てる文字列」として扱うのを即座にやめるべきだ。SQLは「安全なパラメータをバインドして生成する構造体」として扱わなければならない。
—
2. 解決策:型安全な「SQLリテラル・ヘルパー関数」の設計
この問題を根底から解決するため、プロジェクト全体で共通利用できる「型安全なSQL値変換ヘルパー(`SqlLiteral`)」を標準モジュールに実装する。
このモジュールの役割はシンプルだ。どんなデータ型(Variant)が放り込まれても、SQLの構文ルールに従った「安全なリテラル文字列」に変換して返すこと。これだけだ。
プロダクションコード:`modSqlHelper`(標準モジュール)
Option Explicit
Option Compare Database
‘ =========================================================================
‘ モジュール名: modSqlHelper
‘ 概要: 動的SQL生成時のクォーテーション地獄を回避するための型安全リテラル変換
‘ =========================================================================
Public Function SqlLiteral(ByVal vValue As Variant) As String
‘ 1. Null、または長さ0の文字列(Zero-Length String)の処理
If IsNull(vValue) Then
SqlLiteral = “Null”
Exit Function
End If
If VarType(vValue) = vbString Then
If vValue = “” Then
SqlLiteral = “Null” ‘ 空文字をNullとして扱う仕様(業務要件に合わせて変更可)
Exit Function
End If
End If
‘ 2. データ型に応じた適切なSQLリテラルの構築
Select Case VarType(vValue)
‘ 数値型 (Byte, Integer, Long, Currency, Single, Double, Decimal)
Case vbByte, vbInteger, vbLong, vbCurrency, vbSingle, vbDouble, vbDecimal
SqlLiteral = CStr(vValue)
‘ 日付時刻型 (Date)
‘ Access(Jet/ACE)のSQLでは、日付はシャープ(#)で囲み、ISO8601形式(yyyy-mm-dd hh:nn:ss)にするのが鉄則
Case vbDate
SqlLiteral = “#” & Format$(vValue, “yyyy-mm-dd hh:nn:ss”) & “#”
‘ ブール型 (Boolean)
‘ Accessでは True は -1、False は 0 として評価されるが、明示的に文字列表現にする
Case vbBoolean
If vValue Then SqlLiteral = “True” Else SqlLiteral = “False”
‘ 文字列型 (String)
‘ 最重要: SQLインジェクションと構文エラーを防ぐため、シングルクォーテーションをエスケープ(‘ -> ”)
Case vbString
SqlLiteral = “‘” & EscapeSqlString(CStr(vValue)) & “‘”
‘ その他(オブジェクト型や配列など予期せぬ型)
Case Else
‘ 暗黙の型変換を試みる
SqlLiteral = “‘” & EscapeSqlString(CStr(vValue)) & “‘”
End Select
End Function
‘ =========================================================================
‘ 内部関数: 文字列内のシングルクォーテーションのエスケープ
‘ =========================================================================
Private Function EscapeSqlString(ByVal s As String) As String
‘ Access SQLではシングルクォーテーションを2つ重ねることでエスケープする
EscapeSqlString = Replace(s, “‘”, “””)
End Function
—
3. 実践:QueryDefとヘルパー関数を組み合わせた堅牢なクエリ実行
ヘルパー関数を手に入れた私たちのコードは、どれほど美しく、堅牢になるか。
実際の業務ツールを想定したフォームからの検索処理を例に見てみよう。
Private Sub btn検索_Click()
On Error GoTo ErrorHandler
Dim db As DAO.Database
Dim qdf As DAO.QueryDef
Dim sql As String
Dim paramWhere As String
Set db = CurrentDb
‘ 既存の動的クエリ(例: qryTempSearch)を安全に再定義するためのQueryDef活用
‘ ※クエリを直接いじるのではなく、QueryDefオブジェクトを操作する
On Error Resume Next
db.QueryDefs.Delete “qryDynamicSearch”
On Error GoTo ErrorHandler
‘ — 動的WHERE句の構築 —
‘ ここでSqlLiteral関数を挟むことで、型の違いやエスケープを完全に意識から排除できる
paramWhere = “1=1” ‘ 基準条件(ANDで繋ぎやすくするため)
‘ 1. 文字列型の条件(あいまい検索)
If Not IsNull(Me.txt顧客名.Value) And Me.txt顧客名.Value <> “” Then
‘ あいまい検索の場合はワイルドカードをヘルパーの外側で付与し、中身だけをエスケープする
paramWhere = paramWhere & ” AND 顧客名 LIKE ” & SqlLiteral(“%” & Me.txt顧客名.Value & “%”)
End If
‘ 2. 日付型の条件(範囲指定:開始日)
If Not IsNull(Me.txt開始日.Value) Then
paramWhere = paramWhere & ” AND 受注日 >= ” & SqlLiteral(Me.txt開始日.Value)
End If
‘ 3. 数値型の条件(以上指定)
If Not IsNull(Me.txt最低金額.Value) Then
paramWhere = paramWhere & ” AND 合計金額 >= ” & SqlLiteral(Me.txt最低金額.Value)
End If
‘ — SQLの完成 —
sql = “SELECT FROM T_受注明細 WHERE ” & paramWhere & “;”
‘ デバッグ時にイミディエイトウィンドウで確認できるように出力
Debug.Print sql
‘ — QueryDefオブジェクトの生成とフォームへのバインド —
Set qdf = db.CreateQueryDef(“qryDynamicSearch”, sql)
‘ サブフォームのデータソースに動的生成したクエリを指定
Me.sfrm結果.SourceObject = “Query.qryDynamicSearch”
Exit Sub
ErrorHandler:
MsgBox “予期せぬエラーが発生しました。” & vbCrLf & _
“Error: ” & Err.Description, vbCritical, “システムエラー”
End Sub
—
4. チーフアーキテクトからの実務アドバイス
この設計を導入することで、あなたの開発スピードとコードの品質は次元が変わる。最後に、現場でこれを運用する上での重要な注意点をいくつか共有しよう。
1. 「クエリの乱立」に注意せよ:
上記サンプルでは実行時に `CreateQueryDef` でクエリを一時作成している。Accessの仕様上、不要になった一時クエリやオブジェクトのゴミがデータベース内部(システムテーブル)に残ることがある。本格的なエンタープライズ要件では、クエリの新規作成・削除ではなく、「ParamDeFsコレクションを使用した真のパラメータクエリ(Prepared Statement)」を推奨する。しかし、動的な条件項目の有無(AND句の増減)を扱うリッチな検索画面においては、今回紹介した「QueryDefの動的差し替え+SqlLiteralヘルパー」の組み合わせが最もコスパが高く、実用的な着地点となる。
2. 日付のフォーマットはISO8601 (`yyyy-mm-dd`) を厳守せよ:
PCのOS側の地域設定(コントロールパネルの言語・地域)に依存した日付書式(`mm/dd/yyyy` や `yy/mm/dd` など)をそのままSQLに埋め込むと、環境が変わった途端にデータ不整合やクエリ破綻を起こす。ヘルパー内で `Format$(vValue, “yyyy-mm-dd hh:nn:ss”)` と明示的にフォーマットを固定しているのはそのためだ。この規エを破るな。
クォーテーションの迷宮に怯える日々は、今日で終わりだ。
型を制し、ロジックを磨き、誰よりもエレガントなAccessアプリケーションを構築してほしい。
