はじめに:泥臭い文字列連結の果てにある破滅を回避せよ
業務システムの最前線で数十年稼働し続けるAccessアプリケーション。そのソースコードを開いた瞬間、我々シニアエンジニアが目にするのは、幾重にも重なった文字列連結のミルフィーユコードだ。
‘ 破滅への片道切符:現場に散見されるコード
strSQL = “SELECT FROM T_Order WHERE CustomerName = ‘” & Replace(txtCustomer.Value, “‘”, “””) & “‘ AND OrderDate = #” & txtDate.Value & “#”
「シングルクォーテーションを二重化しているからサニタイズは完璧だ」と豪語するエンジニアに出会うたび、私は静かに頭痛を覚える。このアプローチには、本質的な欠陥が3点存在する。
1. ヌル値(Null)の伝播破壊:UI未入力時のVariant/Nullが文字列結合によって空文字 `””` に化け、データベースのThree-Valued Logic(3値論理)を破綻させる。
2. 日付リテラルのロケール依存:Windows APIおよびOSの地域設定に引きずられ、`#01/02/2026#` が1月2日なのか2月1日なのかJET/ACEエンジンが誤認する。
3. BSTRアロケーションのスパイク:大量のSQL文字列結合は、COMの基本文字列型であるBSTRのメモリ再確保(SysAllocStringLen / SysFreeString)を際限なく呼び出し、ヒープの断片化を加速させる。
本来であれば、ACEエンジンが提供する静的クエリ(`DAO.QueryDef`)にパラメーターをバインドするのがセオリーである。しかし、検索画面の任意条件抽出、可変長の`IN (…)`句、実行時ソート列の切り替えなど、現実のエンタープライズ要件では動的SQLの生成を完全に排除することは不可能である。
本稿では、レガシーAccess保守の修羅場をくぐり抜けてきたアーキテクトの視点から、「型安全」「自動エスケープ」「メモリ最適化」を完全にカプセル化したSQLビルダー・クラスの設計と実装を提示する。
—
1. ACE/JETエンジンの内部仕様とエスケープの本質
実装に入る前に、AccessのバックエンドであるACE(Access Connectivity Engine)がSQL文字列をパースするメカニズムを理解しなければならない。
シングルクォートと識別子
JET/ACE SQLにおいて、文字列リテラルはシングルクォート `’` またはダブルクォート `”` で囲む仕様となっている。しかし、ダブルクォートをVBAコード内で扱う場合、VBA構文自体のエスケープ(`””””`)と競合し、コードの認知的負荷が跳ね上がる。したがって、動的SQL内ではシングルクォートを用い、リテラル内部のシングルクォートは2連続(`”`)にするのが業界標準の作法である。
NullとDBNullの取り扱い
SQLにおいて、`WHERE CustomerName = NULL` は常にUNKNOWN(偽)を返す。正しくは `WHERE CustomerName IS NULL` と表現されなければならない。連結時にNullが混入した際、適切な構文へ自動分岐させるロジックをSQL生成機構そのものに持たせる必要がある。
BSTRのメモリコストを軽視するな
VBAの文字列型(`String`)は実質的にWindows APIの `BSTR`(COM文字列)である。
文字列を `&` で連結するたびに、OLEオートメーションライブラリは以下の処理をOSに要求する。
1. 新しいサイズのヒープ領域確保
2. 既存文字列および追加文字列のメモリコピー(`memcpy`)
3. 旧メモリ領域の破棄
このメモリのchurn(激しい確保・破棄)は、ガベージコレクションを持たないネイティブ参照カウント方式のVBA環境において、Accessの「2GB制限」に近づくにつれ深刻なパフォーマンス低下とクラッシュを引き起こす。したがって、ビルダー内部での文字列蓄積には配列バッファリング戦略を取る。
—
2. 決定版:クラスモジュール `clsSqlBuilder` の設計
動的SQLの安全かつ高パフォーマンスな構築を担保するため、`clsSqlBuilder` を定義する。
このクラスは、パラメーターを型情報とともにバッファへ蓄積し、最終的なSQLを構築する責務を負う。
クラスモジュールのコード:`clsSqlBuilder`
AccessのVBAプロジェクトにクラスモジュールを追加し、名称を `clsSqlBuilder` に設定して以下のコードを配置する。
VERSION 1.0 CLASS
BEGIN
MultiUse = -1 ‘True
END
Attribute VB_Name = “clsSqlBuilder”
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = False
Attribute VB_PredeclaredId = False
Attribute VB_Exposed = False
Option Compare Database
Option Explicit
‘ ==============================================================================
‘ クラス名: clsSqlBuilder
‘ 責務: 動的SQL構築におけるインジェクション防御、型安全リテラル整形、メモリ効率化
‘ ==============================================================================
‘ メモリ再確保を最小化するための動的配列バッファ設定
Private Const BUFFER_INITIAL_SIZE As Long = 16
Private Const BUFFER_EXPANSION_FACTOR As Long = 2
Private m_Clauses() As String
Private m_ClauseCount As Long
Private m_Capacity As Long
‘ クラスのライフサイクル管理
Private Sub Class_Initialize()
m_Capacity = BUFFER_INITIAL_SIZE
ReDim m_Clauses(0 To m_Capacity – 1)
m_ClauseCount = 0
End Sub
Private Sub Class_Terminate()
‘ 明示的な内部バッファの解放(SysFreeStringの確実なトリガー)
Erase m_Clauses
m_ClauseCount = 0
m_Capacity = 0
End Sub
‘ ——————————————————————————
‘ 内部メソッド: 配列バッファの動的拡張
‘ ——————————————————————————
Private Sub EnsureCapacity()
If m_ClauseCount >= m_Capacity Then
m_Capacity = m_Capacity BUFFER_EXPANSION_FACTOR
ReDim Preserve m_Clauses(0 To m_Capacity – 1)
End If
End Sub
‘ ——————————————————————————
‘ 内部メソッド: 安全な文字列リテラル整形(自動エスケープ)
‘ ——————————————————————————
Private Function FormatStringLiteral(ByVal value As String) As String
‘ シングルクォートの二重化
‘ Null文字(Chr(0))はJETのパースをクラッシュさせる潜在的脅威のため除去
Dim sanitized As String
sanitized = Replace(value, vbNullChar, vbNullString)
sanitized = Replace(sanitized, “‘”, “””)
FormatStringLiteral = “‘” & sanitized & “‘”
End Function
‘ ——————————————————————————
‘ 内部メソッド: ISO 8601準拠の日付リテラル整形(JET/ACE専用構文)
‘ ——————————————————————————
Private Function FormatDateLiteral(ByVal value As Date) As String
‘ Windowsロケール設定に依存しない形式を強制: #yyyy-mm-dd hh:nn:ss#
FormatDateLiteral = “#” & Format$(value, “yyyy-mm-dd hh:nn:ss”) & “#”
End Function
‘ ——————————————————————————
‘ 公開メソッド: 生のSQL句の追加(SELECT, FROM句など)
‘ ——————————————————————————
Public Sub AppendRaw(ByVal rawSql As String)
EnsureCapacity
m_Clauses(m_ClauseCount) = rawSql
m_ClauseCount = m_ClauseCount + 1
End Sub
‘ ——————————————————————————
‘ 公開メソッド: 等価条件句の追加(型安全・Null考慮)
‘ 列名, 値, 演算子(省略時は = )
‘ ——————————————————————————
Public Sub AddCondition( _
ByVal columnName As String, _
ByVal val As Variant, _
Optional ByVal op As String = “=”)
Dim clause As String
If IsNull(val) Or IsEmpty(val) Then
If op = “=” Then
clause = columnName & ” IS NULL”
ElseIf op = “<>” Then
clause = columnName & ” IS NOT NULL”
Else
‘ 比較演算子とNullの併用は構文的に無意味なため無視、または独自エラー
Exit Sub
End If
Else
Select Case VarType(val)
Case vbString
clause = columnName & ” ” & op & ” ” & FormatStringLiteral(CStr(val))
Case vbDate
clause = columnName & ” ” & op & ” ” & FormatDateLiteral(CDate(val))
Case vbBoolean
‘ Access SQLにおける真偽値リテラル: True / False
clause = columnName & ” ” & op & ” ” & IIf(val, “True”, “False”)
Case vbByte, vbInteger, vbLong, vbSingle, vbDouble, vbCurrency, vbDecimal
‘ ロケール起因のカンマ/ピリオド問題を排除するためStr$を利用しつつ余分な空白を除去
clause = columnName & ” ” & op & ” ” & Trim$(Str$(val))
Case Else
‘ 未定義の型は安全のため文字列扱いとしてエスケープ
clause = columnName & ” ” & op & ” ” & FormatStringLiteral(CStr(val))
End Select
End If
AppendRaw clause
End Sub
‘ ——————————————————————————
‘ 公開メソッド: 可変長IN句の生成
‘ 配列を受け取り、安全にエスケープされたINリストを生成
‘ ——————————————————————————
Public Sub AddInClause(ByVal columnName As String, ByVal values As Variant)
If Not IsArray(values) Then
Err.Raise vbObjectError + 1001, “clsSqlBuilder”, “Array expected for IN clause.”
End If
Dim lb As Long: lb = LBound(values)
Dim ub As Long: ub = UBound(values)
If lb > ub Then Exit Sub ‘ 空配列
Dim formattedList() As String
ReDim formattedList(lb To ub)
Dim i As Long
For i = lb To ub
Dim v As Variant: v = values(i)
If IsNull(v) Or IsEmpty(v) Then
formattedList(i) = “NULL”
Else
Select Case VarType(v)
Case vbString
formattedList(i) = FormatStringLiteral(CStr(v))
Case vbDate
formattedList(i) = FormatDateLiteral(CDate(v))
Case vbByte, vbInteger, vbLong, vbSingle, vbDouble, vbCurrency, vbDecimal
formattedList(i) = Trim$(Str$(v))
Case Else
formattedList(i) = FormatStringLiteral(CStr(v))
End Select
End If
Next i
Dim inClause As String
inClause = columnName & ” IN (” & Join(formattedList, “, “) & “)”
AppendRaw inClause
End Sub
‘ ——————————————————————————
‘ 公開メソッド: 最終SQLのビルド
‘ 引数: 句を結合するデリミタ(通常はスペースや改行)
‘ ——————————————————————————
Public Function Build(Optional ByVal delimiter As String = ” “) As String
If m_ClauseCount = 0 Then
Build = vbNullString
Exit Function
End If
‘ 有効要素のみを切り出してJoin(超高速文字列結合)
Dim result() As String
ReDim result(0 To m_ClauseCount – 1)
Dim i As Long
For i = 0 To m_ClauseCount – 1
result(i) = m_Clauses(i)
Next i
Build = Join(result, delimiter)
End Function
‘ ——————————————————————————
‘ 公開メソッド: 状態のリセット(インスタンス再利用用)
‘ ——————————————————————————
Public Sub Clear()
m_ClauseCount = 0
m_Capacity = BUFFER_INITIAL_SIZE
ReDim m_Clauses(0 To m_Capacity – 1)
End Sub
—
3. 実践:ビルダークラスと名無しQueryDefの融合
SQLインジェクション対策を施した文字列が生成できたとしても、それを `DoCmd.RunSQL` や `CurrentDb.OpenRecordset(strSQL)` に直接食わせるだけでは片手落ちだ。
プロフェッショナルの現場では、「一時クエリ(Temporary QueryDef)」 を利用して実行計画のコンパイルをACEエンジンに促し、レコードセットを取得した直後にリソースを解放する。
以下は、画面の検索フォームから呼び出されるビジネスロジックの完全な実装例である。
標準モジュールでの呼び出し実装
Option Compare Database
Option Explicit
Public Sub ExecuteSafeDynamicQuery()
Dim db As DAO.Database
Dim qdf As DAO.QueryDef
Dim rs As DAO.Recordset
Dim builder As clsSqlBuilder
‘ 1. インスタンス化
Set builder = New clsSqlBuilder
‘ 2. UI等からの入力値(悪意のある入力やNullを含むシミュレーション)
Dim inputCustomer As Variant: inputCustomer = “O’Connor & Sons; DROP TABLE T_Order; –”
Dim inputStartDate As Variant: inputStartDate = #2026/02/01#
Dim inputStatusList As Variant: inputStatusList = Array(“Active”, “Pending’Review”, 999)
Dim inputNullableMemo As Variant: inputNullableMemo = Null
‘ 3. 流れるようなSQL構築
builder.AppendRaw “SELECT OrderID, CustomerName, OrderDate, StatusCode”
builder.AppendRaw “FROM T_Order”
builder.AppendRaw “WHERE”
‘ 型を自動判定し、エスケープを施す
builder.AddCondition “CustomerName”, inputCustomer
builder.AppendRaw “AND”
builder.AddCondition “OrderDate”, inputStartDate, “>=”
builder.AppendRaw “AND”
builder.AddInClause “StatusCode”, inputStatusList
builder.AppendRaw “AND”
builder.AddCondition “InternalMemo”, inputNullableMemo ‘ IS NULL に自動展開
Dim finalSQL As String
finalSQL = builder.Build(vbCrLf)
Debug.Print “=== 生成されたSQL ===”
Debug.Print finalSQL
Debug.Print “=====================”
‘ 4. DAOオブジェクトの厳格なライフサイクル管理
On Error GoTo ErrorHandler
Set db = CurrentDb()
‘ 名前を空文字(“”)にすることで、データベースに保存されない「一時QueryDef」を生成
‘ これにより、MsysObjectsを汚染せず、かつACEのオプティマイザを活用可能
Set qdf = db.CreateQueryDef(vbNullString, finalSQL)
‘ パススルーや大容量データに対するフォワードオンリー・読み取り専用の最適化
Set rs = qdf.OpenRecordset(dbOpenForwardOnly, dbReadOnly)
‘ 5. データフェッチ処理
Do Until rs.EOF
‘ 処理の実行
Debug.Print rs.Fields(“OrderID”).Value, rs.Fields(“CustomerName”).Value
rs.MoveNext
Loop
CleanUp:
‘ 【COMの鉄則】逆順での完全明示的破棄
On Error Resume Next
If Not rs Is Nothing Then
rs.Close
Set rs = Nothing
End If
If Not qdf Is Nothing Then
qdf.Close
Set qdf = Nothing
End If
If Not db Is Nothing Then
Set db = Nothing
End If
Set builder = Nothing
Exit Sub
ErrorHandler:
MsgBox “クエリ実行エラー: ” & Err.Number & vbCrLf & Err.Description, vbCritical, “System Error”
Resume CleanUp
End Sub
生成されるSQLの出力結果
上記コードを実行した際、イミディエイトウィンドウに出力されるSQLは以下のようになる。
SELECT OrderID, CustomerName, OrderDate, StatusCode
FROM T_Order
WHERE
CustomerName = ‘O”Connor & Sons; DROP TABLE T_Order; –‘
AND
OrderDate >= #2026-02-01 00:00:00#
AND
StatusCode IN (‘Active’, ‘Pending”Review’, 999)
AND
InternalMemo IS NULL
いかなる入力であってもSQLコンテキストが破壊されず、シングルクォートのエスケープ、日付リテラルのISO標準化、Nullの3値論理対応が完全に自動化されていることが確認できる。
—
4. 運用と保守の極意:アーキテクトが配慮すべき深層
1. `CurrentDb()` と参照カウントの罠
`CurrentDb()` 関数は、呼び出されるたびに新しいデータベースオブジェクトのインスタンスを生成する。
コード内で `CurrentDb.CreateQueryDef(…)` と直接チェーンすると、親となるインスタンスがスコープから外れ、ポインタが宙に浮く現象(未定義動作)を引き起こす可能性がある。
必ずローカルの `Dim db As DAO.Database` 変数で受け、使い終わったら `Set db = Nothing` で解放すること。
2. インジェクション対策の境界線(LIKE句のワイルドカード)
本クラスはシングルクォートのエスケープを自動化するが、`LIKE` 演算子を使用する場合は別次元の考慮が必要となる。
ACE SQLにおける `LIKE` 演算子では、“, `?`, `#`, `[` などの記号がワイルドカードとして機能する。
これらの文字自体を検索対象とする場合は、エスケープ文字 `[]` や `[?]` への置換処理をビルダーに追加実装する必要がある。業務要件で `LIKE` による部分一致検索を多用する場合は、`AddLikeCondition` といった専用メソッドをクラスに拡張せよ。
3. ODBCパススルークエリへの応用
バックエンドがSQL ServerやPostgreSQLの場合、ACE特有の日付構文 `#yyyy-mm-dd#` は構文エラーとなる。
システムがODBCパススルーを前提とする場合は、`clsSqlBuilder` に `TargetRdbms` プロパティを持たせ、日付リテラルの出力を標準SQLの形式(例: `’2026-02-01 00:00:00’`)へ切り替える設計パターンが極めて効果的だ。
—
終わりに:アーキテクチャが技術的負債を駆逐する
動的SQLの引用符エスケープ問題を「開発者の注意力」に依存させる組織は、遅かれ早かれ重大なインシデントに直面する。人間の注意深さは有限だが、正しくカプセル化されたクラスの振る舞いは不変である。
レガシーなAccess環境だからこそ、最新のソフトウェア工学の知見を注ぎ込むべきだ。
この `clsSqlBuilder` をプロジェクトに組み込み、散らばった `Replace(…, “‘”, “””)` を今すぐリファクタリングしていただきたい。それが、システムの寿命をさらに10年延ばす、真のエンジニアリングである。
