【テクニカル・上級編】ADOの接続プールを強制的に解放する:長時間稼働するAccessアプリのメモリ管理術 – Access VBA解析バイブル

スポンサーリンク
スポンサーリンク

コネクションプールの亡霊を屠る:長時間稼働する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を用いて、システムが「生きている」ことを数値で証明せよ。

これらを徹底することで、君の構築するシステムは、レガシーな環境においても、鋼のように強固で、揺るぎない安定性を手にするだろう。

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