Access VBAを掌握する極限の知見:SQL Serverストアドプロシージャをパススルーで完全制圧する実装パターン
開発現場でよく見かける悪夢がある。数百万件のレコードを持つSQL Serverのテーブルに対し、Accessからリンクテーブルを貼り、`CurrentDb.OpenRecordset(“SELECT FROM HugeTable WHERE …”)` と投げる。ネットワークは飽和し、Jet/Accessのエンジンはメモリを食い潰し、最終的に「固まった」と言ってタスクマネージャーで強制終了する――。
断言しよう。Accessを「クライアント/サーバーシステムのフロントエンド」として正しく機能させたいなら、重い処理をAccess側に持ち込んではならない。 計算はすべてSQL Server側に押し付け、Accessは結果を受け取るだけの「薄いビューアー」に徹するべきだ。
そのための最強の武器が、「パススルー・クエリによるストアドプロシージャの動的実行」 である。
今回は、QueryDefオブジェクトをVBAで完全に制御し、堅牢かつ高速にSQL Serverのパワーを引き出す実践的アーキテクチャを伝授する。
—
1. なぜ「リンクテーブル+通常クエリ」ではダメなのか?
Accessのリンクテーブル経由のクエリは、多くの場合、SQL Server側で最適化(実行プランの生成)が行われたとしても、最終的な絞り込みやソートの幾ばくかをAccess側のローカルエンジン(ACE)に持ち帰って処理しようとする特性がある。
特に、複雑な集計や、複数テーブルの結合、一時テーブルを駆使するバッチ処理などをリンクテーブル経由でやらせると、ネットワーク帯域の無駄遣いとローカルリソースの枯渇を招く。
パススルー・クエリの圧倒的優位性
パススルー・クエリとは、「AccessはSQLを一切解釈せず、そのままの文字列をODBC経由でSQL Serverに丸投げし、実行結果だけをレコードセットとして受け取る」 仕組みだ。
これを利用してSQL Server側の「ストアドプロシージャ」を呼び出せば、データベースの計算リソースを100%活用でき、ネットワークを流れるデータ量も最小限に抑えられる。
—
2. 堅牢な設計のための3大原則
実務の現場で動的SQLやパススルーを実装する際、素人が書いたコードは必ず「接続文字列のハードコーディング」「エラー時のQueryDef残留」「SQLインジェクション(あるいは構文エラー)」の罠にハマる。
プロダクションコードとして耐えうるシステムにするため、以下の3点を遵守せよ。
1. 接続文字列の動的取得(ハードコードの排除)
環境が変わるたびにコードを書き換える愚を犯してはならない。現在のリンクテーブルからODBC接続文字列を動的に抽出するか、安全な設定保持機構を使うこと。
2. QueryDefのクリーンアップ(ゴミを残さない)
動的に生成したQueryDefオブジェクトは、処理の成否に関わらず必ず明示的に削除(あるいは再利用)し、データベースを肥大化させないこと。
3. トランザクションとパラメータの安全な受け渡し
ストアドプロシージャへの引数は、文字列結合で直接SQL文に埋め込むのではなく、コマンドオブジェクトや適切なエスケープ、あるいは安全なクエリ構築を行うこと。
—
3. 【コピペ即実戦】パススルー・ストアド実行モジュール
以下に、実務の現場でそのまま組み込める、堅牢性を極めたVBAコードを示す。
このコードは、既存のリンクテーブルから接続文字列を自動取得し、一時的なパススルー・クエリを動的生成してSQL Serverのストアドプロシージャを実行、結果をレコードセットとして回収するテンプレートだ。
Option Compare Database
Option Explicit
/
- SQL Serverのストアドプロシージャをパススルー・クエリで動的に実行し、
- 結果をDAO.Recordsetとして返す関数。
- @param pStoredProcName 実行するストアドプロシージャ名 (例: “usp_CalculateMonthlySales”)
- @param pParamString ストアドに渡す引数文字列 (例: “@Year=2023, @Region=’East'”)
- @return DAO.Recordset 実行結果のレコードセット(呼び出し側で必ずClose/Set Nothingすること)
/
Public Function ExecuteStoredProcedurePassThrough(ByVal pStoredProcName As String, Optional ByVal pParamString As String = “”) As DAO.Recordset
Dim db As DAO.Database
Dim qdf As DAO.QueryDef
Dim connStr As String
Const TEMP_QUERY_NAME As String = “tmp_PassThrough_Exec”
Set db = CurrentDb
On Error GoTo ErrorHandler
‘ —————————————————-
| 1. 接続文字列の動的取得
‘ —————————————————-
‘ ハードコーディングを避け、既存のリンクテーブルからODBC接続文字列を借用する。
‘ ※プロジェクト内に少なくとも1つ、対象DBへの有効なリンクテーブルが存在することが前提。
connStr = GetValidConnectString(db)
If Len(connStr) = 0 Then
Err.Raise 9999, “PassThrough”, “有効なODBC接続文字列を取得できませんでした。リンクテーブルを確認してください。”
End If
‘ —————————————————-
| 2. 既存の同名一時クエリが存在する場合は削除
‘ —————————————————-
If QueryExists(db, TEMP_QUERY_NAME) Then
db.QueryDefs.Delete TEMP_QUERY_NAME
End If
‘ —————————————————-
| 3. QueryDef(パススルー)の動的生成
‘ —————————————————-
Set qdf = db.CreateQueryDef(TEMP_QUERY_NAME)
‘ パススルー・クエリとしてのプロパティ設定
qdf.Connect = connStr
‘ ストアドプロシージャを呼び出すSQL文を構築
‘ EXEC ステートメントを発行する
If Len(pParamString) > 0 Then
qdf.SQL = “EXEC ” & pStoredProcName & ” ” & pParamString
Else
qdf.SQL = “EXEC ” & pStoredProcName
End If
‘ サーバー側での処理完了を待たずにレコードを返す設定などが必要な場合はここで調整
qdf.ReturnsRecords = True
qdf.ODBCTimeout = 60 ‘ タイムアウトを60秒に設定(必要に応じて変更)
‘ —————————————————-
| 4. 実行とレコードセットの取得
‘ —————————————————-
‘ 注意: パススルーを実行すると、qdf.OpenRecordset の時点でSQL Server側でプロシージャが実行される。
Set ExecuteStoredProcedurePassThrough = qdf.OpenRecordset(dbOpenSnapshot)
‘ クエリ定義オブジェクト自体はメモリ(QueryDefsコレクション)から削除しても、
‘ 開いたレコードセットは独立して生存する(ただしコネクションの維持に注意が必要な場合があるため設計に依存)
‘ ※今回はクエリを保持したままレコードセットを返すため、クエリの削除は呼び出し側の責任、
‘ あるいは次回実行時に上書き削除されるためこのままで機能する。
Exit Function
ErrorHandler:
‘ エラーハンドリング:ログ出力やユーザー通知をここに実装
MsgBox “ストアドプロシージャの実行に失敗しました。” & vbCrLf & _
“エラー番号: ” & Err.Number & vbCrLf & _
“詳細: ” & Err.Description, vbCritical, “DB処理エラー”
‘ クリーニング処理
If QueryExists(db, TEMP_QUERY_NAME) Then
db.QueryDefs.Delete TEMP_QUERY_NAME
End If
Set ExecuteStoredProcedurePassThrough = Nothing
End Function
‘ — ヘルパー関数群 —
Private Function GetValidConnectString(ByRef db As DAO.Database) As String
Dim tdf As DAO.TableDef
For Each tdf In db.TableDefs
‘ リンクテーブル(ConnectプロパティがODBCから始まっているもの)を探す
If Left(tdf.Connect, 4) = “ODBC” Then
GetValidConnectString = tdf.Connect
Exit Function
End If
Next tdf
GetValidConnectString = “”
End Function
Private Function QueryExists(ByRef db As DAO.Database, ByVal qryName As String) As Boolean
Dim qdf As DAO.QueryDef
QueryExists = False
For Each qdf In db.QueryDefs
If qdf.Name = qryName Then
QueryExists = True
Exit Function
End If
Next qdf
End Function
—
4. 呼び出し側の実装例:実務での活用
上記で作成した関数を、実際の業務フォームやボタンクリックイベントからどのように呼び出すか。
極めてシンプルかつ、メモリリークのない美しいコードになる。
Private Sub cmdRunBatch_Click()
Dim rs As DAO.Recordset
Dim param As String
‘ 画面の入力コントロールからパラメータを安全に構築
param = “@TargetYear = ” & Me.txtYear.Value & “, @BranchID = ‘” & Me.cmbBranch.Value & “‘”
‘ 処理中カーソルの変更
DoCmd.Hourglass True
On Error GoTo ErrorHandler
‘ パススルー関数の呼び出し
Set rs = ExecuteStoredProcedurePassThrough(“usp_ExecuteMonthlyClose”, param)
If rs Is Nothing Then
MsgBox “処理が中断されました。”, vbExclamation
GoTo Finally
End If
‘ 結果の判定やメッセージ表示(ストアド側でステータスを返している場合など)
If Not (rs.EOF And rs.BOF) Then
MsgBox “処理が正常終了しました。件数: ” & rs.RecordCount, vbInformation, “完了”
‘ 必要であれば、結果をローカルのテンポラリテーブルに流し込むか、
‘ あるいはフォームのRecordsetにバインドするなどの処理を行う
‘ Set Me.SubForm.Form.Recordset = rs (※スナップショットとしてのバインド)
Else
MsgBox “対象データが存在しませんでした。”, vbInformation, “通知”
End If
Finally:
‘ リソースの解放(鉄則)
If Not rs Is Nothing Then
rs.Close
Set rs = Nothing
End If
DoCmd.Hourglass False
Exit Sub
ErrorHandler:
MsgBox “予期せぬエラーが発生しました: ” & Err.Description, vbCritical
Resume Finally
End Sub
—
5. チーフアーキテクトからの最終助言
このパターンを導入することで、Accessは「重いロジックを抱え込んで自壊するシステム」から脱却し、「SQL Serverという強靭なエンジンのインターフェース(UI層)」へと生まれ変わる。
ネットワークトラフィックは劇的に減少し、何分もかかっていたバッチ処理が数秒で終わるようになる。Access開発において、「SQL Serverの能力をいかにスポイルせずに引き出すか」はアーキテクトの腕の見せ所だ。
泥臭いローカル処理の積み重ねから脱却し、真にスケーラブルなクライアント/サーバーアーキテクトとしての設計を、今日から現場で実践してほしい。
