コネクションプールの亡霊を屠る:長時間稼働するAccessアプリのための極限メモリ管理術
Access VBAを「使い捨てのツール」として設計しているうちは、まだ初心者だ。
真にプロフェッショナルなエンジニアが向き合うのは、工場のラインや物流センターの管理端末のように、「一度起動したら数週間、あるいは数ヶ月間、一度も再起動せずに動き続ける」過酷なレガシー環境だ。
こうした環境で、エンジニアを最も苦しめるのは、目に見えるバグではない。
「数日経つと、なぜか動作が重くなる」「メモリ消費が右肩上がりに増え続け、最終的に `Out of Memory` で沈む」という、静かなる死――すなわち、ADO(ActiveX Data Objects)の接続プールと、それに伴うメモリリークの正体だ。
本稿では、DAOとADOのアーキテクチャの決定的な違いを紐解き、ADOの接続プールを制御し、メモリを完全に掌握するための極限の知見を伝授する。
1. アーキテクチャの深淵:DAOとADOの決定的な違い
メモリ管理を語る前に、我々は戦うべき相手の正体を知らねばならない。Accessにおけるデータアクセスには、DAOとADOという二つの異なる思想が存在する。
DAO (Data Access Objects)
DAOは、ACE/Jetエンジンに密結合した、Access専用の「純正品」だ。
オブジェクトのライフサイクルはエンジンと同期しており、メモリ管理は非常に予測しやすい。ローカルの `.accdb` ファイルを操作する分には、DAOの挙動を制御するのは容易だ。
ADO (ActiveX Data Objects)
一方、ADOは「抽象化の塊」である。
ADOは、背後にある OLE DBプロバイダー(SQL Server Native ClientやMicrosoft OLE DB Provider for SQL Serverなど)を呼び出すためのラッパーに過ぎない。
ここで問題が発生する。「接続を閉じた(`Connection.Close`)」つもりでも、物理的な接続やプロバイダーが確保したメモリ領域は、OLE DBの「接続プール(Connection Pooling)」によって保持され続ける。
ADOにおいて `Set cn = Nothing` を実行することは、VBA上の参照カウントをゼロにするだけであり、プロバイダーが保持するメモリ領域やネットワークソケットを解放することを保証するものではないのだ。
2. 接続プールの罠:なぜメモリは膨張するのか
長時間稼働するアプリケーションにおいて、以下のパターンは「メモリ枯渇への片道切符」となる。
1. 頻繁な接続・切断の繰り返し: ループ内で `New Connection` と `Close` を繰り返すと、接続プールに大量の「休止状態の接続」が蓄積される。
2. 接続文字列の微細な差異: 接続文字列のパラメータ(`Application Name` や `Workstation ID` など)が動的に変わると、プロバイダーはそれらを「別の接続」と判断し、新しいプールを作成してしまう。
3. オブジェクトの解放順序の誤り: `Recordset` が `Connection` よりも先に、かつ確実に破棄されていない場合、参照が残り続け、プールが解放されない。
3. 実装:極限のメモリ管理を実現する「ConnectionManager」
単なる `Set cn = Nothing` では足りない。
我々が構築すべきは、オブジェクトのライフサイクルを厳格に管理し、プロバイダーに対して明示的に「これ以上は不要である」と伝える設計である。
以下に、シニアエンジニアが実装すべき、堅牢な接続管理クラスの設計指針を示す。
接続管理クラス(Class Module: `clsDBManager`)
Option Compare Database
Option Explicit
' ======================================================================================
' Class: clsDBManager
' Purpose: ADO接続のライフサイクルを厳格に管理し、メモリリークを防止する
' Author: Legendary Chief Architect
' ======================================================================================
Private mConn As ADODB.Connection
Private mConnString As String
' 初期化時に接続文字列を保持
Public Sub Initialize(ByVal strConn As String)
mConnString = strConn
End Sub
' 接続の確立
Public Function GetConnection() As ADODB.Connection
On Error GoTo Err_Handler
' 既存の接続があれば一旦破棄(二重接続の防止)
If Not mConn Is Nothing Then
CloseConnection
End If
Set mConn = New ADODB.Connection
' パフォーマンスとメモリ管理の鍵:
' 接続文字列に Pooling=False を含める手法もあるが、
' 通常はプロバイダーの挙動に任せつつ、明示的な Close を徹底するのが定石。
mConn.ConnectionString = mConnString
mConn.CursorLocation = adUseClient ' クライアントサイドカーソルを使用し、サーバー負荷を抑える
mConn.Open
Set GetConnection = mConn
Exit Function
Err_Handler:
MsgBox "Connection Error: " & Err.Description, vbCritical
Set GetConnection = Nothing
End Function
' 接続とオブジェクトの完全な破棄
Public Sub CloseConnection()
On Error Resume Next
' 1. Connectionオブジェクトが存在する場合のみ処理
If Not mConn Is Nothing Then
' 2. 接続を閉じる(これによりプロバイダーへ切断を通知)
If mConn.State = adStateOpen Then
mConn.Close
End If
' 3. 参照を完全に抹消(COMオブジェクトの参照カウントをゼロにする)
Set mConn = Nothing
End If
' 4. ガベージコレクションを促すための明示的な Nothing
' VBAではこれだけでメモリが即座にOSに返るわけではないが、
' ライフサイクル管理の観点から必須の作法である。
On Error GoTo 0
End Sub
' クラスが破棄される際(Endステートメントや変数のスコープ終了時)に確実に実行
Private Sub Class_Terminate()
CloseConnection
End Sub
実用的な呼び出しパターン
重要なのは、`Connection` オブジェクトをグローバル変数として放置しないことだ。「必要な時だけ生成し、使い終わったら即座に破棄する」。これが鉄則である。
Public Sub ExecuteBatchProcess()
Dim dbMgr As clsDBManager
Dim rs As ADODB.Recordset
Dim cn As ADODB.Connection
Set dbMgr = New clsDBManager
dbMgr.Initialize "Provider=SQLOLEDB;Data Source=SERVER_NAME;Initial Catalog=DB_NAME;Integrated Security=SSPI;"
On Error GoTo Err_Handler
' 接続取得
Set cn = dbMgr.GetConnection()
If cn Is Nothing Then Exit Sub
' データ操作
Set rs = New ADODB.Recordset
rs.Open "SELECT * FROM HeavyTable", cn, adOpenStatic, adLockReadOnly
' --- 何らかの重い処理 ---
' ...
' 【極限の作法】解放の順序は「Recordset → Connection」
' RecordsetがConnectionを参照しているため、逆は許されない。
rs.Close
Set rs = Nothing
' Connectionの解放(ConnectionManager経由で安全に実行)
dbMgr.CloseConnection
Set dbMgr = Nothing
Exit Sub
Err_Handler:
MsgBox "Critical Error: " & Err.Description
' エラー時も確実に解放を行う
If Not rs Is Nothing Then Set rs = Nothing
If Not dbMgr Is Nothing Then dbMgr.CloseConnection
Set dbMgr = Nothing
End Sub
4. Windows APIによるメモリ監視(高度なデバッグ)
もし、上記の実装を行ってもメモリが増え続ける場合、それはADOではなく、他のCOMコンポーネントや、DLLのメモリリークを疑う必要がある。
プロフェッショナルは、VBA内からWindows APIを呼び出し、プロセスの物理メモリ使用量を監視する。
' Windows API: プロセスのメモリ情報を取得するための宣言
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
Private Declare PtrSafe Function GetProcessMemoryInfo Lib "psapi.dll" ( _
ByVal hProcess As LongPtr, _
ByRef lpcm As PROCESS_MEMORY_COUNTERS, _
ByVal cb As Long) As Long
Private Declare PtrSafe Function GetCurrentProcess Lib "kernel32" () As LongPtr
' 現在のプロセスが使用している物理メモリ(Working Set)をMB単位で返す
Public Function GetCurrentProcessMemoryMB() As Double
Dim pmc As PROCESS_MEMORY_COUNTERS
Dim hProcess As LongPtr
hProcess = GetCurrentProcess()
pmc.cb = Len(pmc)
If GetProcessMemoryInfo(hProcess, pmc, pmc.cb) <> 0 Then
GetCurrentProcessMemoryMB = Round(pmc.WorkingSetSize / 1024 / 1024, 2)
Else
GetCurrentProcessMemoryMB = 0
End If
End Function
この関数を、定期的なログ出力や、メモリ閾値を超えた際の警告アラートに組み込むことで、「いつ、どの処理でメモリが跳ね上がったのか」を完全に可視化できる。
結論:支配者への道
Access VBAにおけるメモリ管理の本質は、単なるコードの記述ではない。「オブジェクトの生存期間(Lifetime)を完全にコントロール下に置く」という哲学にある。
1. DAOとADOの差を理解せよ。 ADOの背後にはプロバイダーの「意志(プール)」があることを忘れるな。
2. カプセル化せよ。 接続管理をクラス(`clsDBManager`)に閉じ込め、生存期間を明確にせよ。
3. 解放の順序を遵守せよ。 `Recordset` $\to$ `Connection` の順序を違えれば、参照カウントの迷宮に迷い込む。
4. 可視化せよ。 APIを用いて、システムが「生きている」ことを数値で証明せよ。
これらを徹底することで、君の構築するシステムは、レガシーな環境においても、鋼のように強固で、揺るぎない安定性を手にするだろう。
