揮発するメモリ、沈黙する接続:長時間稼働AccessアプリにおけるADOアーキテクチャの極致
Accessアプリケーションが「業務の心臓部」として機能し続けるとき、エンジニアが直面する真の敵は、ロジックのバグではない。それは「エントロピーの増大」、すなわち、数ヶ月、数年という単位で稼働し続けるプロセスの中で蓄積される、微細なメモリリークと、制御不能に陥った接続(Connection)の残骸である。
多くの開発者は、`Set cn = Nothing` と書けばメモリが解放されると信じている。しかし、それはCOM(Component Object Model)の深淵を理解していない者の浅はかな幻想だ。
本稿では、ADO(ActiveX Data Objects)を用いた大規模・長時間稼働システムにおいて、接続プーリングを最大限に活用しながら、メモリの整合性を極限まで維持するためのアーキテクチャ戦略を詳説する。
1. DAOとADO:アーキテクチャの決定的な乖離
まず、我々が扱う武器の性質を再定義しておく必要がある。
- DAO (Data Access Objects): ACE/Jetエンジンに密結合しており、ローカルの`.accdb`ファイルを操作する際のオーバーヘッドは最小。しかし、ネットワーク越し、あるいは異種データベースへの拡張性には限界がある。
- ADO (ActiveX Data Objects): OLE DBプロバイダを介した抽象化レイヤー。接続文字列一つでSQL ServerやOracleを操作できるが、その抽象化こそが「メモリ管理の複雑化」を招く。
長時間稼働するシステムにおいて、外部DBへの接続にADOを採用する場合、我々は「プロバイダが管理するリソース」と「VBAが管理するオブジェクト」の二重のライフサイクルを制御しなければならない。
2. 接続プーリングの正体と「破壊」の作法
ADOの真価は、OLE DBレベルでの接続プーリング(Connection Pooling)にある。
開発者が `Connection.Open` を呼び出し、`Close` を実行したとき、物理的なネットワーク接続は即座に切断されるわけではない。それは、OLE DBプロバイダが管理する「プール」へと返却され、次の `Open` 時に再利用されるために待機している。
ここで、シニアエンジニアが陥る致命的なミスが二つある。
ミスA:接続文字列の不一致によるプールの無効化
プーリングは、「完全に一致する接続文字列」に対してのみ機能する。
例えば、ある関数では `Provider=SQLOLEDB;Data Source=ServerA;…` と書き、別の関数で `Provider=SQLOLEDB; Data Source=ServerA;…` (スペースの有無)と書いた場合、プーリングは機能せず、毎回物理的なハンドシェイクが発生する。これはパフォーマンスの低下だけでなく、サーバー側の接続リソースを枯渇させる原因となる。
ミスB:オブジェクトの「不完全な破棄」
`Connection.Close` は接続をプールに戻すが、`Connection` オブジェクト自体(COMインスタンス)をメモリから抹消するわけではない。VBAの参照カウントが正しくゼロにならなければ、オブジェクトはメモリ上に幽霊のように居座り続け、これが積み重なることでAccessのワーキングセット(Working Set)は膨張していく。
3. 極限のメモリ管理戦略:クラスモジュールによるカプセル化
生の `ADODB.Connection` をコードの至る所に散布することは、設計上の自殺行為である。我々は、接続のライフサイクルを「決定論的」に制御するために、ラッパークラスを導入しなければならない。
以下に、接続プーリングを最適化しつつ、メモリリークを徹底的に抑止する設計パターンを示す。
実装例:`clsDBManager` (Class Module)
' =================================================================
' Class Module: clsDBManager
' Purpose: ADO接続のライフサイクル管理とメモリ最適化
' =================================================================
Option Explicit
Private m_conn As ADODB.Connection
Private m_connectionString As String
' 接続文字列を定数または設定ファイルから取得し、一貫性を保証する
Private Sub Class_Initialize()
' 本来は設定ファイル等から読み込む。ここでは固定値。
' 文字列の完全一致を維持することが、プーリング成功の絶対条件。
m_connectionString = "Provider=SQLOLEDB;Data Source=YOUR_SERVER;Initial Catalog=YOUR_DB;Integrated Security=SSPI;"
End Sub
' 接続の取得(プーリングの活用)
Public Function GetConnection() As ADODB.Connection
On Error GoTo ErrorHandler
' すでに接続が存在し、開いている場合はそれを再利用
If Not m_conn Is Nothing Then
If m_conn.State = adStateOpen Then
Set GetConnection = m_conn
Exit Function
End If
End If
' 新規接続の生成
Set m_conn = New ADODB.Connection
m_conn.ConnectionString = m_connectionString
m_conn.CursorLocation = adUseClient ' クライアントサイドカーソルによる柔軟な操作
m_conn.Open
Set GetConnection = m_conn
Exit Function
ErrorHandler:
' エラー発生時はオブジェクトを確実に初期化し、メモリリークを防ぐ
Set m_conn = Nothing
Err.Raise Err.Number, "clsDBManager.GetConnection", Err.Description
End Function
' 接続の明示的な破棄(ここが極めて重要)
Public Sub CloseConnection()
On Error Resume Next ' 破棄プロセス中のエラーは無視する
If Not m_conn Is Nothing Then
' 1. 物理接続をプールに返す
If m_conn.State <> adStateClosed Then
m_conn.Close
End If
' 2. COMオブジェクトの参照カウントをゼロにする
Set m_conn = Nothing
End If
On Error GoTo 0
End Sub
' クラスが破棄される際の最終防衛線
Private Sub Class_Terminate()
Call CloseConnection
End Sub
実装の要諦:
1. `adUseClient` の選択: 大規模なデータセットを扱う際、サーバーサイドカーソルはサーバーのリソースを占有し続けるリスクがある。クライアントサイドカーソル(`adUseClient`)を利用し、データをメモリ上にロードした後は速やかに `Recordset` を閉じることで、サーバーとの接続を解放する。
2. `Class_Terminate` の活用: `Set obj = Nothing` を呼び忘れても、クラスのスコープが外れれば `Class_Terminate` が走り、`CloseConnection` が実行される。これにより、エラー発生時でもリソース解放の確実性が飛躍的に高まる。
4. Windows APIを用いたプロセスの健全性監視
シニアエンジニアであれば、VBAの内部数値だけでなく、OSレベルの視点を持つべきだ。Accessがどれほどのメモリを消費しているか、`GetProcessMemoryInfo` を用いて監視する仕組みを組み込むことで、異常なメモリ増大を検知し、自動でアプリを再起動、あるいはユーザーに警告を発することが可能になる。
' 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 lpcmdv As PROCESS_MEMORY_COUNTERS, _
ByVal cb As Long) As Long
Private Declare PtrSafe Function GetCurrentProcess Lib "kernel32" () As LongPtr
' 現在のワーキングセット(物理メモリ使用量)を取得する関数
Public Function GetCurrentWorkingSet() As LongPtr
Dim pmc As PROCESS_MEMORY_COUNTERS
Dim hProcess As LongPtr
hProcess = GetCurrentProcess()
pmc.cb = Len(pmc)
If GetProcessMemoryInfo(hProcess, pmc, pmc.cb) <> 0 Then
GetCurrentWorkingSet = pmc.WorkingSetSize
Else
GetCurrentWorkingSet = 0
End If
End Function
この関数を定期的なタイマー(`Application.OnTime`)で実行し、`WorkingSetSize` が閾値を超えた場合に、現在のトランザクションを安全に終了させてからプロセスをクリーンアップする――これこそが、プロフェッショナルが設計する「止まらないシステム」の姿である。
結言
Access VBAにおけるADOの制御は、単なる「コードの書き方」の範疇を超え、「リソースの統治」という領域に属する。
接続プーリングを理解し、オブジェクトのライフサイクルをクラスによって厳格に管理し、OSレベルの挙動を監視する。この三位一体の設計思想こそが、レガシーな環境においても、現代的なエンタープライズ・アプリケーションに匹敵する堅牢性を実現する唯一の道である。
妥協は、静かなるメモリリークという形で、必ずシステムを蝕む。
