亡霊を屠る:Access VBAにおけるDAO Recordsetのライフサイクル制御とメモリ管理の極致
多くの開発者は、`Set rs = Nothing` と記述すれば、そのオブジェクトがメモリから消滅すると信じて疑わない。
もし君が、大規模な業務システムにおいて、数千件、数万件のデータを扱うフォームを動的に切り替える設計をしているのなら、その甘い認識は致命的なシステムダウンを招く。Access VBAの世界において、オブジェクトの破棄は「宣言」ではなく「儀式」である。
本稿では、DAO(Data Access Objects)のRecordsetをフォームのデータソースとして動的に割り当てる際、メモリリークという名の「亡霊」をいかにして完全に排除し、極限まで軽量な動作を実現するか、その真髄を説く。
1. 誤解の淵:なぜ `Set = Nothing` だけでは足りないのか
VBAはCOM(Component Object Model)を基盤としている。Recordsetのような重厚なオブジェクトは、VBAの変数領域だけでなく、背後のACE(Access Connectivity Engine)というエンジン層において、膨大なメモリ領域とファイルハンドルを確保している。
`Set rs = Nothing` は、あくまで「VBAの変数からオブジェクトへの参照を断ち切る」だけであり、「エンジンが確保したリソースを解放せよ」という命令ではない。
特に、フォームの `Me.Recordset` プロパティにDAO Recordsetを代入する場合、オブジェクトの所有権(Ownership)の所在が、VBAの変数とフォームオブジェクトの間で複雑に遷移する。この遷移過程において、明示的な `.Close` を怠ると、参照カウントがゼロにならず、メモリ上に「解放されないRecordset」が蓄積され続ける。これが、長時間稼働させたシステムが徐々に重くなり、最終的に `Out of Memory` で沈む正体だ。
2. アーキテクチャの急所:Recordsetの動的切り替えにおける鉄則
フォームのデータソースを動的に書き換える際、我々が戦うべきは「メモリの断片化」と「ハンドルリーク」である。以下のプロセスを「アトミック(不可分)」に実行することが、プロフェッショナルの作法である。
破棄の黄金律
1. 既存のRecordsetの明示的クローズ: `Me.Recordset.Close` を呼び出し、エンジン側のリソースを強制解放する。
2. 参照の切断: `Set Me.Recordset = Nothing` により、フォームとの紐付けを完全に断つ。
3. 新しいRecordsetの生成と代入: 新しいリソースを割り当てる。
4. ローカル変数のクリーンアップ: 代入に使用したローカル変数も、スコープを抜ける前に確実に処理する。
3. 実装:極限のメモリ管理パターン
以下に、シニアエンジニアが現場で実装すべき、堅牢なRecordset切り替えロジックを示す。単なる代入ではなく、エラーハンドリングとリソース管理を統合した「アーキテクトのコード」だ。
Option Compare Database
Option Explicit
' ======================================================================================
' Module: modDataArchitecture
' Description: 高信頼性データソース切り替えモジュール
' ======================================================================================
''' <summary>
''' フォームのRecordsetを安全に動的切り替えする。
''' メモリリークを徹底的に防ぐためのクローズ・プロトコルを実装。
''' </summary>
''' <param name="frm">対象となるフォームオブジェクト</param>
''' <param name="strSql">取得するSQL文</param>
''' <param name="db">使用するDatabaseオブジェクト</param>
Public Sub SwitchFormRecordset(ByRef frm As Form, ByVal strSql As String, ByRef db As DAO.Database)
On Error GoTo ErrorHandler
Dim rsNew As DAO.Recordset
' --- Phase 1: 既存リソースの徹底排除 ---
' フォームに既にRecordsetが紐付いている場合、それを「殺す」
If Not frm.Recordset Is Nothing Then
On Error Resume Next ' クローズ失敗によるエラーを無視し、強制的に進める
frm.Recordset.Close
Set frm.Recordset = Nothing
On Error GoTo ErrorHandler
End If
' --- Phase 2: 新規リソースの構築 ---
' dbオブジェクトは呼び出し元で管理されていることが前提
Set rsNew = db.OpenRecordset(strSql, dbOpenDynaset)
' --- Phase 3: 所有権の委譲 ---
' Recordsetをフォームに代入。ここから、Recordsetの管理権はフォームに移る。
Set frm.Recordset = rsNew
' 【重要】rsNewへの参照をここで切断する。
' これにより、Recordsetの生存期間はフォームの生存期間と同期する。
Set rsNew = Nothing
ExitProcedure:
Exit Sub
ErrorHandler:
' エラー発生時には、中途半端な状態のオブジェクトを確実に解放する
MsgBox "Critical Error in Recordset Switching: " & Err.Description, vbCritical, "Architecture Failure"
If Not rsNew Is Nothing Then
On Error Resume Next
rsNew.Close
Set rsNew = Nothing
End If
Resume ExitProcedure
End Sub
4. 深層:Windows APIによる「監視」という視点
シニアエンジニアであれば、コードを書くだけでなく、システムが「物理的にどうなっているか」を疑うべきだ。
VBA単体ではメモリ使用量を詳細に追うことは難しいが、Windows APIを用いて、プロセスが消費している `Working Set`(物理メモリ)を監視することで、自作した切り替えロジックが正しく機能しているかを検証できる。
メモリリークが発生している場合、Recordsetを切り替えるたびに、プロセスのメモリ使用量が階段状に上昇していく。
' Windows APIの宣言(メモリ監視用・デバッグ時のみ使用)
If VBA7 Then
Private Declare PtrSafe Function GetProcessMemoryInfo Lib "psapi.dll" ( _
ByVal hProcess As LongPtr, _
lpWorkingSetX As PROCESS_MEMORY_COUNTERS, _
ByVal cb As Long) As Long
Private Declare PtrSafe Function GetCurrentProcess Lib "kernel32" () As LongPtr
Else
Private Declare Function GetProcessMemoryInfo Lib "psapi.dll" ( _
ByVal hProcess As Long, _
lpWorkingSetX As PROCESS_MEMORY_COUNTERS, _
ByVal cb As Long) As Long
Private Declare Function GetCurrentProcess Lib "kernel32" () As Long
End If
Private Type PROCESS_MEMORY_COUNTERS
cb As Long
PageFaultCount As Long
PeakWorkingSetSize As LongPtr
WorkingSetSize As LongPtr
QuotaPeakPagedPoolUsage As LongPtr
QuotaPagedPoolUsage As LongPtr
QuotaPeakNonPagedPoolUsage As LongPtr
QuotaNonPagedPoolUsage As LongPtr
PagefileUsage As LongPtr
PeakPagefileUsage As LongPtr
End Type
''' <summary>
''' 現在のプロセスの物理メモリ使用量(Working Set)を返す。
''' メモリリークの検知に用いる。
''' </summary>
Public Function GetCurrentWorkingSet() As LongPtr
Dim pmc As PROCESS_MEMORY_COUNTERS
Dim hProcess As LongPtr
hProcess = GetCurrentProcess()
If GetProcessMemoryInfo(hProcess, pmc, LenB(pmc)) <> 0 Then
GetCurrentWorkingSet = pmc.WorkingSetSize
Else
GetCurrentWorkingSet = 0
End If
End Function
この関数を、Recordset切り替えの前後に配置し、`Debug.Print` で出力せよ。切り替え後にメモリ使用量が戻っていない、あるいは増え続けているなら、君の設計にはまだ「亡霊」が潜んでいる。
5. 結論:アーキテクトへの提言
Access VBAにおける「動的なデータソース切り替え」は、一見すると便利な機能に見える。しかし、その裏側では、COMオブジェクトの参照カウント、ACEエンジンのメモリ管理、そしてWindowsのプロセス管理という、重層的なリソースの綱渡りが行われている。
- `Close` は `Nothing` よりも重い。
- 所有権の委譲(Assignment)が発生する場所を常に意識せよ。
- 「動く」ことと「正しく管理されている」ことは全く別物である。
これらを徹底することで、君のシステムはレガシーな環境においても、数年、十数年という歳月を耐え抜く強靭なアーキテクチャへと昇華されるだろう。
技術の真髄は、華やかな機能実装ではなく、こうした目に見えない「管理」の徹底にこそ宿るのだ。
