Access VBAを掌握する極限の知見:QueryDefとRecordsetの完全解放、その「先」にあるメモリリーク撲滅の全手順
開発現場でよく見かける光景がある。
「`Set rs = Nothing` を書いておけば、メモリリークは起きない」
――甘い。実務で何十万件ものレコードを扱い、長時間のバッチ処理や常時稼働する業務システムを構築するアーキテクトから見れば、それは「気休め」に過ぎない。
Access VBA(Jet/ACEエンジン)における `QueryDef` と `Recordset` の組み合わせは、強力な動的SQL生成・実行の武器となる反面、オブジェクトのライフサイクルとCOMコンポーネントの参照カウンタの仕組みを正しく理解していないと、確実にメモリリークを引き起こし、やがて「リソース不足」という致命的なエラーをもたらす。
今回は、`Set Nothing` を書くだけでは防げない背徳のメモリリークのメカニズムを暴き、プロダクション環境で絶対に破綻しない「完全解放の全手順」をロジカルに伝授しよう。
—
1. なぜ「Set rs = Nothing」だけでは不十分なのか?
VBAの背後で動いているDAO(Data Access Objects)やADODBは、WindowsのCOM(Component Object Model)に基づいている。ここで重要なのは、「変数を破棄すること」と「内部リソースを解放すること」は同義ではないという点だ。
隠れた落とし穴:コレクションへの参照と暗黙のQueryDef
動的SQLを生成する際、以下のようなコードを書いていないだろうか?
‘ 【アンチパターン】これはいけない
Dim db As DAO.Database
Set db = CurrentDb
‘ 1回限りの使い捨てのつもりでQueryDefsに追加せず、直接Recordsetを開こうとして…
いや、もっと厄介なのは `CurrentDb.QueryDefs` を使って一時的なクエリを生成・実行するパターンだ。
`QueryDef` オブジェクトを明示的に `Delete` しないまま、あるいは `Database` オブジェクトのスコープを残したままループ処理を回すと、Accessの内部キャッシュ(SysCache)にゴミが蓄積し続ける。
さらに、`Recordset` を閉じる(`Close`)順番を間違えたり、エラーハンドリング内で解放処理がバイパスされたりすると、メモリ上にCOMの実体がゾンビのように居座り続けることになる。
—
2. 堅牢なリソース管理の3大鉄則
プロダクションコードを書く上で、以下の3つは絶対のルールである。
1. 生成と逆順の破棄(LIFOの原則)
開いた順番の「逆」の順序で閉じる(`Recordset` ➔ `QueryDef` ➔ `Database`)。
2. エラーセーフな解放(Error-Safe Cleanup)
途中で例外(実行時エラー)が発生しても、確実に解放処理へジャンプする構造(`On Error GoTo` による集約)を徹底する。
3. カプセル化と即時破棄
不必要にグローバルなスコープを持たせず、プロシージャのスコープ内で完結させ、スコープアウトする瞬間に全リソースを無力化する。
—
3. 【コピペ推奨】バグの起きないプロダクションコード例
パラメータクエリを動的に生成し、安全に実行して `Recordset` を取得、そして確実にリソースを消却する模範的なプロシージャを提示する。
Option Explicit
‘ =========================================================================
‘ 概要: 動的パラメータクエリを安全に実行し、Recordsetを取得・処理する模範ルーチン
‘ 備考: メモリリークを完全に防ぐため、逆順のクローズと確実なオブジェクト破棄を実装
‘ =========================================================================
Public Sub ExecuteDynamicQuerySample()
Dim db As DAO.Database
Dim qdf As DAO.QueryDef
Dim rs As DAO.Recordset
‘ 一時クエリの名前(競合を防ぐためユニークな名前か、プレフィックスを推奨)
Const QRY_NAME As String = “tmp_DynamicQuery”
On Error GoTo ErrorHandler
‘ 1. データベース参照の取得
Set db = CurrentDb
‘ 2. 既存の同名一時クエリがあれば削除(クリーンアップの徹底)
On Error Resume Next
db.QueryDefs.Delete QRY_NAME
On Error GoTo ErrorHandler
‘ 3. QueryDefの動的作成(パラメータクエリの構築)
‘ ※SQLインジェクションを防ぐため、値は直接埋め込まずパラメータオブジェクトを使用する
Set qdf = db.CreateQueryDef(QRY_NAME, _
“PARAMETERS p_ParamText Text(255); ” & _
“SELECT FROM T_Master WHERE CategoryName = [p_ParamText];”)
‘ 4. パラメータへの値代入
qdf.Parameters(“p_ParamText”).Value = “文房具”
‘ 5. Recordsetの取得(スナップショットで高速化&メモリ節約)
Set rs = qdf.OpenRecordset(dbOpenSnapshot)
‘ — データ処理ループ —
If Not (rs.BOF And rs.EOF) Then
rs.MoveFirst
Do Until rs.EOF
‘ デバッグ出力(実務ではここに業務ロジックが入る)
Debug.Print rs.Fields(“ItemName”).Value
rs.MoveNext
Loop
End If
CleanUp:
‘ =====================================================================
‘ 【極限の知見】確実なリソース解放の全手順(生成の逆順)
‘ =====================================================================
‘ ① Recordsetの閉鎖と解放
If Not rs Is Nothing Then
rs.Close
Set rs = Nothing
End If
‘ ② QueryDefの削除と解放(永続化させない一時クエリの場合)
‘ ※これを怠るとAccessのシステムテーブルやメモリを圧迫します
On Error Resume Next
If Not qdf Is Nothing Then
db.QueryDefs.Delete QRY_NAME
Set qdf = Nothing
End If
On Error GoTo 0
‘ ③ Databaseの解放(CurrentDbの場合は必須ではないが、明示的に破棄することで安全性を高める)
Set db = Nothing
Exit Sub
ErrorHandler:
‘ 異常系ハンドリング
MsgBox “予期せぬエラーが発生しました: ” & Err.Description, vbCritical, “システムエラー”
Resume CleanUp
End Sub
このコードが「プロ仕様」である理由
1. `db.QueryDefs.Delete` による一時クエリの消去
動的に作成した `QueryDef` は、明示的に削除しない限りAccessのコンテナ(SysObjects)に残り続け、ファイルサイズ肥大化(Bloat)とメモリリークの温床になる。処理の最後(あるいは開始時)に必ず削除する設計にしている。
2. `dbOpenSnapshot` の活用
更新が必要ない参照系のクエリであれば、`dbOpenDynaset` よりも `dbOpenSnapshot` を使うべきだ。ロックを伴わず、メモリ消費量も最小限に抑えられるため、パフォーマンスが劇的に向上する。
3. `CleanUp` ラベルへの一本化
正常終了であっても異常終了であっても、必ず `CleanUp` ラベルを通過するように `On Error GoTo ErrorHandler` と `Resume CleanUp` を組み合わせている。これにより、例外発生時にオブジェクトが宙ぶらりんになるのを完全に防いでいる。
—
四流のプログラマーは「動けばいい」と言い、二流のプログラマーは「`Set Nothing` を書けばいい」と言う。
そして、一流のアーキテクトは、「OSとアプリケーションのメモリ境界を意識し、いかなる例外が起きようともリソースを完全に無に戻すコード」を書く。
Accessというコンパクトでありながらスパルタンな環境だからこそ、こうした極限のメモリ管理思想がシステムの寿命を決定づける。あなたの開発する業務システムに、この知見を今すぐインストールしてほしい。
