【テクニカル・上級編】QueryDefの実行結果をRecordsetとして取得する際、メモリリークを確実に防ぐオブジェクト解放の全手順 – Access VBA解析バイブル

スポンサーリンク

Access VBAを掌握する極限の知見:QueryDefとRecordsetが生むメモリリークを根絶する全手順

シニアエンジニアや大規模なAccessシステムを保守するアーキテクトであれば、一度は直面したことがあるはずだ。
「なぜか数日稼働させるとメモリ使用量が右肩上がりになり、最終的にCOM例外やリソース不足でVBEがクラッシュする」
「`Set rs = Nothing` を書いているにもかかわらず、OSのプロセスレベルでメモリが解放されない」

Access VBA(DAO)における `QueryDef` と `Recordset` の組み合わせは、一見するとシンプルに見えるが、その裏側にあるCOM(Component Object Model)の参照カウンティング、Jet/ACEデータベースエンジンのキャッシュ機構、そしてVBA特有のガベージコレクションの未熟さを理解していなければ、確実にメモリリークを引き起こす。

今回は、単なる「`Set Nothing` を書きましょう」といった入門レベルの話ではない。VBAのメモリ管理の深層に踏み込み、実務の現場で絶対に破綻しないリソース解放の全手順を解説する。

—

1. なぜ「`Set Nothing`」だけではメモリリークを防げないのか?

VBAの裏側では、DAOオブジェクトはCOMコンポーネントとして動作している。変数に `Nothing` を代入するということは、厳密には「その変数からオブジェクトへの参照を断ち切る(参照カウントを1つ減らす)」ことを意味する。

しかし、以下の要因により、メモリリークやポインタの残存が発生する。

1. 暗黙の参照保持(QueryDefとRecordsetの親子関係)
`CurrentDb.QueryDefs(“q_name”).OpenRecordset()` のように記述した場合、`QueryDef` オブジェクト自体を明示的な変数に格納していないため、参照を明示的に解放する手段を失う。これがいわゆる「名前のないオブジェクト」のメモリリークの温床となる。
2. ACEエンジン内部のキャッシュ
DAOが発行したクエリプランやレコードのキャッシュは、VBAの変数が消滅しても、データベースセッションが維持されている間はメモリ上に居座り続ける。
3. COMの解放順序のミス
`Recordset` を閉じる前に親である `Database` や `QueryDef` を破棄したり、逆に不適切な順序で参照を切断すると、COMランタイムが正しくメモリを回収できなくなる。

—

2. 現場で実証された「完璧なオブジェクト解放」の作法

複雑なパラメータクエリを動的に生成し、実行結果を `Recordset` として取得、処理した後に一滴の水滴すらメモリ上に残さないための極限のコードパターンを提示する。

以下のコードは、エラーハンドリング(`On Error Goto`)が発動した場合であっても、確実にリソースが解放される構造(Try-FinallyイディオムのVBA実装)を厳守している。

Option Compare Database
Option Explicit

Public Sub ExecuteQueryDefSafely(ByVal paramValue As String)
‘ 宣言セクション:すべてのDAOオブジェクトは先頭でNothing初期化する
Dim ws As DAO.Workspace
Dim db As DAO.Database
Dim qdf As DAO.QueryDef
Dim rs As DAO.Recordset

Dim isErrorOccurred As Boolean
isErrorOccurred = False

On Error GoTo ErrorHandler

‘ 1. ワークスペースとデータベースの明示的参照
‘ CurrentDbを毎回呼び出すと、その度に新しいDAO.Databaseインスタンスが生成され
‘ 参照リークの原因になるため、必ず変数に格納して使い回す。
Set ws = DBEngine.Workspaces(0)
Set db = ws.Databases(0)

‘ 2. QueryDefオブジェクトの取得(または動的生成)
‘ ここでは既存のクエリ定義を取得する想定
Set qdf = db.QueryDefs(“q_ParametricTarget”)

‘ 3. パラメータの動的バインド
‘ QueryDef経由でパラメータを設定することで、SQLインジェクションを防ぎつつ
‘ 実行計画の最適化(キャッシュ利用)の恩恵を受ける
qdf.Parameters(“[Forms]![Criteria]![txtParam]”) = paramValue

‘ 4. Recordsetの取得(スナップショットまたはダイナセット)
‘ 読み取り専用であれば dbOpenSnapshot を選択し、メモリオーバヘッドを最小限にする
Set rs = qdf.OpenRecordset(dbOpenSnapshot, dbReadOnly)

‘ — 業務ロジックの実行 —
If Not (rs.BOF And rs.EOF) Then
rs.MoveFirst
Do Until rs.EOF
‘ TODO: データの処理
‘ Debug.Print rs.Fields(“TargetCol”).Value
rs.MoveNext
Loop
End If

CleanUp:
‘ ==========================================

  • 厳格なリソース解放の順序(逆順解放の原則)

‘ ==========================================
‘ 1. Recordsetのクローズと解放
If Not rs Is Nothing Then
rs.Close
Set rs = Nothing
End If

‘ 2. QueryDefの解放
If Not qdf Is Nothing Then
qdf.Close
Set qdf = Nothing
End If

‘ 3. Databaseの解放(CurrentDb変数の場合も明示的に閉じる・解放する)
If Not db Is Nothing Then
db.Close
Set db = Nothing
End If

‘ 4. Workspaceの解放
Set ws = Nothing

‘ エラー伝播
If isErrorOccurred Then
Err.Raise Err.Number, Err.Source, Err.Description
End If
Exit Sub

ErrorHandler:
isErrorOccurred = True
‘ エラーログ出力や固有のハンドリングをここに記述
MsgBox “予期せぬエラーが発生しました: ” & Err.Description, vbCritical
Resume CleanUp
End Sub

—

3. チーフアーキテクトが教える「3つの鉄則」

上記のコードを支える、実務上の極意を言語化する。

鉄則 1: `CurrentDb` の安易な多用を絶対に禁止する

コード内で `CurrentDb.QueryDefs…` や `CurrentDb.OpenRecordset…` と直接記述しているコードをよく見かけるが、これは極悪なアンチパターンである。
`CurrentDb`メソッドは呼び出されるたびに内部で新しい `Database` オブジェクトのインスタンスを生成する。これを変数に受けずに放置すると、VBAが終了するまでメモリ上にゴミ(ゾンビインスタンス)が残り続ける。
必ず `Set db = CurrentDb`(または `DBEngine.Workspaces(0).Databases(0)`)として変数に収め、最後に `.Close` と `Set db = Nothing` を実行すること。

鉄則 2: `OpenRecordset` の引数を最適化せよ

`QueryDef.OpenRecordset` を実行する際、デフォルト引数に頼るな。

  • データの更新が不要なら、必ず `dbOpenSnapshot` を指定する。
  • サーバー側(ODBC/SQL Server等)のデータを扱うパススルーやリンクテーブルの場合、トランザクションやロックのスコープを意識し、`dbReadOnly` や `dbForwardOnly`(可能な場合)を組み合わせてメモリ消費量を極限まで削れ。

鉄則 3: クエリ定義の使い回しと動的パラメータの作法

アプリケーションの実行中に何度も `QueryDef` オブジェクトを `CreateQueryDef` で動的作成・破棄を繰り返すと、Accessの内部システムテーブル(MSysObjectsなど)が肥大化し、データベースファイルそのものが破損する原因になる。
動的SQLを実行したい場合でも、一時的なQueryDefを毎回作るのではなく、あらかじめ雛形となるクエリをコンパイル済みの状態で保持しておき、`Parameters` コレクション経由で値を流し込む設計(上記コードのスタイル)を徹底すること。

—

4. 極限環境下でのメモリ最適化:Windows APIの活用(上級者向け)

数百万件のレコードを処理するバッチ処理などで、VBAのガベージコレクションが追いつかず、どうしてもメモリが解放しきれない極限状態に陥った場合、VBAのランタイムに強制的にメモリ解放を促す手法がある。

あまり多用すべきではないが、レガシーシステムの常時稼働プロセスにおいては、以下のAPIを組み合わせることでプロセスのワーキングセットをクリーンに保つことが可能だ。

‘ 32bit/64bit両対応のAPI宣言
If VBA7 Then
Private Declare PtrSafe Function SetProcessWorkingSetSize Lib “kernel32” ( _
ByVal hProcess As LongPtr, _
ByVal dwMinimumWorkingSetSize As LongPtr, _
ByVal dwMaximumWorkingSetSize As LongPtr) As Long
Private Declare PtrSafe Function GetCurrentProcess Lib “kernel32” () As LongPtr
Else
Private Declare Function SetProcessWorkingSetSize Lib “kernel32” ( _
ByVal hProcess As Long, _
ByVal dwMinimumWorkingSetSize As Long, _
ByVal dwMaximumWorkingSetSize As Long) As Long
Private Declare Function GetCurrentProcess Lib “kernel32″ () As Long
End If

”’

”’ プロセスのメモリ使用量を強制的にOSに返還させる(最終手段)
”’

Public Sub ForceReleaseMemory()
Dim hProc As LongPtr
hProc = GetCurrentProcess()
‘ ワーキングセットのサイズを最小化し、余剰メモリをOSへ解放させる
Call SetProcessWorkingSetSize(hProc, -1, -1)
End Sub

※注意: このAPIはOSのメモリマネージャーに強制的にプレッシャーを与えるため、頻繁な呼び出しは逆にパフォーマンスの低下を招く。重いバッチ処理のイテレーションの最後や、一連の巨大な `QueryDef`/`Recordset` 処理チェーンが完全に終了した直後の1回だけに留めること。

—

結びにかえて

Access VBAは、その手軽さゆえに「動けばいい」というコードが量産されやすい。しかし、エンタープライズの現場や基幹システムの一部として組み込まれた瞬間、メモリリークはシステム全体の致命傷となる。

`QueryDef` と `Recordset` を使いこなすとは、単にSQLを書くことではない。背後でうごめくCOMのライフサイクルとメモリの潮流を完全に支配することを意味する。
今回解説した「逆順解放の原則」と「オブジェクト変数の厳格なスコープ管理」をあなたのコードベースに導入し、一切の妥協のない堅牢なシステム構築を実現してほしい。

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