【テクニカル・上級編】実行時エラーを「SQLの構文解析」で先読みする:QueryDef実行前のバリデーションロジック – Access VBA解析バイブル

スポンサーリンク

Access VBAを掌握する極限の知見:実行時エラーを「SQLの構文解析」で先読みするバリデーションアーキテクチャ

レガシーシステムの深部において、Access VBAとDAO(Data Access Objects)の組み合わせは、依然として業務の根幹を支える強烈なエンジンである。しかし、動的に構築されるSQL文字列の海に溺れ、「実行時エラー ‘3146’:ODBCの呼び出しが失敗しました」あるいは「構文エラー(演算子Missing)」といった曖昧なメッセージをユーザーに突きつけた瞬間、そのシステムの信頼性は地に落ちる。

アマチュアの開発者は、SQLを実行してからエラーハンドリングで捕捉しようとする。だが、プロフェッショナルなアーキテクトは違う。「実行する前に、DAOのエンジン自身にSQLを評価させ、破綻を未然に防ぐ」。

今回は、QueryDefのライフサイクルを極限まで制御し、実行前のSQL構文解析によって堅牢性を担保するバリデーションロジックの極意を授ける。

—

1. なぜ「実行時エラー」を事前に叩く必要があるのか

動的SQLの生成において、最大の悪夢は「ユーザーの入力値や条件分岐によって、ごく稀に発生する構文破綻」である。
例えば、カンマの数、クォーテーションのミス、存在しないフィールド名の指定など、これらはVBAのコンパイル時には絶対に検知できない。

DAOの `QueryDef` オブジェクトを永続的にデータベースへ書き込む、あるいは一時的にバインドして実行する際、Jet/ACEエンジンは内部でSQLのパース(構文解析)を行う。この仕組みを逆用し、「実際にデータをフェッチ・更新するコストを払う前に、パース処理だけを安全に実行する」のが本稿の核心である。

—

2. 霊長類最強のバリデーション実装:`ValidateDynamicQuery` パターン

以下のコードは、動的生成されたSQL文字列とパラメータの型整合性を、実際にクエリを実行することなく事前に検証し、詳細なエラーメッセージへと変換する実用的なアーキテクチャである。

Option Compare Database
Option Explicit

‘ =========================================================================
‘ クラス名 / モジュール名: mQueryValidator
‘ 概要: 動的SQLの構文およびパラメータ整合性を事前検証するアーキテクチャ
‘ =========================================================================

Public Function ValidateAndExecuteDynamicQuery( _
ByVal TargetDb As DAO.Database, _
ByVal SQLString As String, _
Optional ByVal ParametersCollection As Collection = Nothing) As Boolean

Const PROC_NAME As String = “ValidateAndExecuteDynamicQuery”
Dim qdfTemp As DAO.QueryDef
Dim prm As DAO.Parameter
Dim item As Variant

On Error GoTo ErrorHandler

‘ 1. 一時QueryDefの生成(名前を空にすることでシステムカタログを汚染しない)
‘ ※DAOの仕様上、名前なしのQueryDefはメモリ上(RAM)だけでライフサイクルが完結する
Set qdfTemp = TargetDb.CreateQueryDef(“”, SQLString)

‘ 2. パラメータのバインドと型の事前チェック
If Not ParametersCollection Is Nothing Then
For Each item In ParametersCollection
‘ 呼び出し側から渡されたパラメータ名と値を動的にバインド
‘ ここでSQL側のパラメータ宣言と不一致があれば即座にトラップされる
For Each prm In qdfTemp.Parameters
If prm.Name = item(0) Then
prm.Value = item(1)
Exit For
End If
Next prm
Next item
End If

‘ 3. 【核心】クエリの構文解析(SQLのパース)のみを実行
‘ ReturnsRecordsプロパティを適切に設定し、不要なデータフェッチコストを排除
If Left$(Trim$(UCase$(SQLString)), 6) = “SELECT” Then
qdfTemp.ReturnsRecords = True
Else
qdfTemp.ReturnsRecords = False
End If

‘ ★ここに注目:実際にレコードセットを開く、あるいは実行するのではなく、
‘ DAOの仕様の隙間をつく「構文の整合性テスト」を実施する。
‘ (厳密なパース検証のためには、一時的なOpenRecordset(dbOpenSnapshot)のプレ実行が最も確実)
Dim rstDummy As DAO.Recordset
If qdfTemp.ReturnsRecords Then
‘ 実行計画の構築と構文解析を強制実行(データは取得せず構造のみ確認)
Set rstDummy = qdfTemp.OpenRecordset(dbOpenSnapshot, dbOpenDatasheetRetrieveAuthors)
rstDummy.Close
Set rstDummy = Nothing
Else
‘ 更新系クエリの場合はトランザクション内でのロールバック前提テスト等も応用可能だが
‘ ここでは構文エラーのみをトラップする
qdfTemp.Execute dbFailOnError
End If

‘ 検証および実行成功
ValidateAndExecuteDynamicQuery = True
GoTo CleanUp

ErrorHandler:
‘ 4. エラーの抽象化と詳細なコンテキスト付与
Dim errDetail As String
Select Case Err.Number
Case 3075 ‘ SQLステートメントの構文エラー
errDetail = “SQLの構文に論理的な誤りがあります。” & vbCrLf & _
“生成されたSQLを確認してください。” & vbCrLf & vbCrLf & _
“【SQL】” & vbCrLf & SQLString
Case 3061 ‘ パラメータが不足しています
errDetail = “必要なパラメータが指定されていません。”
Case Else
errDetail = “予期せぬデータベースエラーが発生しました。” & vbCrLf & _
“Error ” & Err.Number & “: ” & Err.Description
End Select

MsgBox errDetail, vbCritical, “動的クエリ検証エラー [” & PROC_NAME & “]”
ValidateAndExecuteDynamicQuery = False

CleanUp:
‘ 5. 厳格なメモリ解放(オブジェクトのライフサイクル管理)
‘ レガシーVBAにおけるメモリリークを防ぐため、逆順かつ確実に解放する
If Not rstDummy Is Nothing Then
rstDummy.Close
Set rstDummy = Nothing
End If

If Not qdfTemp Is Nothing Then
‘ 名前を持たない一時QueryDefであっても、明示的にDeleteメソッドを呼ぶか
‘ スコープアウトによる自動解放を確実にする
TargetDb.QueryDefs.Delete qdfTemp.Name ‘ 名前が空の場合は注意が必要だが、RAM上のインスタンスはNothingで解放される
Set qdfTemp = Nothing
End If

Exit Function
End Function

—

3. アーキテクチャの深層:メモリ最適化とリソース管理の鉄則

シニアエンジニアであれば、上記のコードにおける「メモリ管理の執念」に気づくだろう。

一時 `QueryDef`(Unnamed QueryDef)の活用

Access VBAにおいて、`CurrentDb.QueryDefs.Append` を安易に繰り返すと、システムカタログ(MSysObjects)が肥大化し、データベース全体のパフォーマンス劣化やファイル破損(Bloat)の原因となる。
`CurrentDb.CreateQueryDef(“”, strSQL)` のように第一引数に空文字列 `””` を渡すことで、ディスクを汚さず、完全なメモリ上(RAM)だけで動作する一時クエリを生成できる。

オブジェクトの明示的破棄とガベージコレクションの限界

VBAのランタイムは参照カウント方式を採用しているが、DAOやCOMコンポーネントの背後にあるC++ネイティブのメモリブロックは、VBAのスコープを抜けただけでは即座に解放されないことがある。
特にループ内で動的SQLの生成・実行を繰り返すバッチ処理では、以下の作法を徹底しなければ確実にメモリリークを引き起こす。

1. レコードセットの明示的クローズ (`rst.Close`)
2. オブジェクト変数の `Nothing` 代入 (`Set rst = Nothing`)
3. 親オブジェクトの再取得回避 (`CurrentDb` をプロパティとして毎回呼ぶと、毎回異なるインスタンスが生成されポインタがリークする。必ず変数にキャッシュせよ)

—

4. チーフアーキテクトからの提言:実務への適用と限界の理解

このバリデーションロジックを導入することで、ユーザーからの「なんか動かないんですけど」という抽象的な問い合わせを、「SQLのここが間違っている」という具体的な開発者フィードバックへと昇華させることができる。

しかし、忘れてはならない。Jet/ACEエンジンのパーサは優秀だが、「RDBの向こう側にある外部ODBCデータソース(SQL ServerやOracleなど)固有の方言(ダイアレクト)」の構文エラーまでは完全に事前検知できない場合がある。
パススルークエリを動的生成する場合は、ODBCドライバ自体のバリデーションフェーズも考慮に入れた、二段構えの例外処理設計こそが、真にエンタープライズに耐えうるAccessシステムの要件となる。

妥協なきコードを書け。エラーは実行されてから慌てて処理するのではなく、実行される前に沈黙させよ。

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