長時間稼働するAccessアプリの命運を分ける:ADO接続プールの「隠れた肥大化」を制するメモリ管理術
業務自動化の現場において、Accessは単なる「小さなデータベース」ではない。それは、数千人のユーザーが、あるいは工場のラインが、数日間、時には数週間、一度も閉じられることなく稼働し続ける「ミッションクリティカルな基幹システム」へと変貌する。
しかし、多くの開発者が陥る致命的な罠がある。それは、「昨日まで動いていたツールが、数日経つと動作が重くなり、最終的にメモリ不足や接続エラーで落ちる」という現象だ。
もし君が、ADO(ActiveX Data Objects)を使ってSQL Server等の外部データベースと連携しているなら、その原因の正体は、ADOの親切心である「接続プーリング(Connection Pooling)」の制御ミスにある可能性が高い。
本稿では、単なるリファレンスの解説ではない。長時間稼働するプロフェッショナルなアプリケーションにおいて、いかにしてメモリを解放し、堅牢な接続ライフサイクルを設計すべきか、その極限の知見を伝授する。
1. 構造的理解:DAOとADOの決定的な「重みの違い」
まず、アーキテクチャの根本的な違いを脳に刻んでおけ。ここを混同している設計者は、一生「原因不明のバグ」に悩まされることになる。
DAO (Data Access Objects)
DAOは、Accessのエンジン(ACE/Jet)と密結合した、いわば「内臓」だ。`.accdb` ファイル内部のテーブルを操作する際、DAOは極めて低レイヤーで動作し、オーバーヘッドが最小限に抑えられている。ローカルなファイル操作においては、DAOこそが最速かつ最も安定した選択肢だ。
ADO (ActiveX Data Objects)
一方でADOは、OLE DBプロバイダーを介して外部の世界(SQL Server, Oracle, Web API等)へ通信するための「外交官」だ。ADOの最大の特徴は、ネットワーク越しに効率よく通信を行うための「接続プーリング」を備えている点にある。
接続を確立するコストは高い。そのため、ADOは一度切断した接続を完全に破棄せず、メモリ上に「プール(待機状態)」として保持する。これが、高速化の恩恵をもたらすと同時に、長時間稼働するAccessアプリにおいては「メモリを食いつぶすゾンビ」へと変貌するのだ。
2. なぜADO接続がメモリを圧迫するのか?
一般的な開発者はこう考える。
`Connection.Close` を呼び、`Set Connection = Nothing` とすれば、接続は完全に消滅するはずだ、と。
それは大きな間違いだ。
`Connection.Close` を実行しても、ADOのプロバイダーは「次にまた同じ接続文字列で接続が来るかもしれない」と考え、物理的な接続を維持し、関連するメモリバロック(バッファ)を保持し続ける。
Accessアプリが数日間、あるいは数週間にわたって、頻繁に「接続→クエリ実行→切断」を繰り返すと、このプール内に「実態のない、しかしメモリを占有し続ける接続オブジェクトの残骸」が蓄積されていく。これが、Accessのメモリリークの正体である。
3. 堅牢な設計指針:Scoped Connection(限定スコープ接続)パターン
長時間稼働するアプリにおいて、接続を「アプリ起動中ずっと保持する(グローバル変数)」のは、設計として極めて脆弱だ。逆に、「プーリングに頼りすぎる」のも危険だ。
私が推奨する設計は、「必要な時に、必要な分だけ、確実に、そして完全に抹殺する」というScoped Connection(限定スコープ接続)パターンである。
設計の3原則
1. Connectionの局所化: 接続オブジェクトを標準モジュールのパブリック変数に置くな。必ずプロシージャ内、あるいはクラスモジュール内で管理せよ。
2. 完全な破壊の順序: Recordsetを先に閉じ、次にConnectionを閉じ、最後にオブジェクトを `Nothing` にする。この順序を一つでも間違えれば、参照カウントがゼロにならず、メモリは解放されない。
3. エラーハンドリングの強制: エラーが発生した瞬間に、オブジェクトの破棄プロセスへジャンプする構造を組み込め。
4. 実戦投入:プロダクション・グレードの接続管理クラス
以下に、私が設計の現場で実際に使用している、メモリ管理を徹底した接続管理クラスのテンプレートを示す。これを標準モジュールから呼び出して使う。
クラスモジュール名: `clsDBManager`
Option Compare Database
Option Explicit
' =============================================================================
' クラス名: clsDBManager
' 目的: ADO接続のライフサイクルを厳格に管理し、メモリリークを防止する
' 設計思想: 接続の局所化と、エラー発生時の確実なオブジェクト破棄
' =============================================================================
Private mConn As ADODB.Connection
' 接続文字列(実際には設定ファイルや定数から取得すること)
Private Const CONN_STR As String = "Provider=SQLOLEDB;Data Source=SERVER_NAME;Initial Catalog=DB_NAME;Integrated Security=SSPI;"
''' <summary>
''' データベース接続を確立する
''' </summary>
Public Function OpenConnection() As Boolean
On Error GoTo ErrorHandler
' 既存の接続があれば一旦破棄(二重接続防止)
Call CloseConnection
Set mConn = New ADODB.Connection
' タイムアウト設定は業務要件に合わせて調整
mConn.ConnectionTimeout = 15
mConn.CommandTimeout = 30
mConn.Open CONN_STR
OpenConnection = True
Exit Function
ErrorHandler:
Debug.Print "【Error】Connection Open Failed: " & Err.Description
OpenConnection = False
Call CloseConnection ' 失敗時も中途半端な状態を避けるためクリーンアップ
End Function
''' <summary>
''' 接続を閉じ、オブジェクトを完全にメモリから解放する
''' </summary>
Public Sub CloseConnection()
On Error Resume Next ' 破棄プロセス中のエラーは無視して完遂させる
If Not mConn Is Nothing Then
' 1. 接続を閉じる
If mConn.State = adStateOpen Then
mConn.Close
End If
' 2. オブジェクト参照を完全に抹消(これが極めて重要)
Set mConn = Nothing
End If
On Error GoTo 0
End Sub
''' <summary>
''' クエリ実行用のRecordsetを取得する(接続は維持)
''' </summary>
Public Function GetRecordset(ByVal sql As String) As ADODB.Recordset
On Error GoTo ErrorHandler
Dim rs As ADODB.Recordset
Set rs = New ADODB.Recordset
' 接続が確立されていない場合はエラー
If mConn Is Nothing Then
Err.Raise vbObjectError + 513, "clsDBManager", "Connection is not established."
End If
' カーソルタイプは必要最小限(adOpenForwardOnly)にすることでメモリ負荷を抑える
rs.Open sql, mConn, adOpenForwardOnly, adLockReadOnly
Set GetRecordset = rs
Exit Function
ErrorHandler:
Debug.Print "【Error】GetRecordset Failed: " & Err.Description
Set GetRecordset = Nothing
End Sub
''' <summary>
''' 破壊子(Class_Terminate)
''' クラスが破棄される際、確実に接続を解放する
''' </summary>
Private Sub Class_Terminate()
Call CloseConnection
End Sub
標準モジュールでの実装例(正しい使い方)
このクラスをどう使うかが、エンジニアの腕の見せ所だ。
Option Compare Database
Option Explicit
''' <summary>
''' 業務処理の実行例
''' </summary>
Public Sub ExecuteBusinessLogic()
' 1. クラスをインスタンス化(スコープをこのプロシージャ内に限定)
Dim db As clsDBManager
Set db = New clsDBManager
Dim rs As ADODB.Recordset
Dim sql As String
' 2. 接続確立
If Not db.OpenConnection() Then
MsgBox "データベース接続に失敗しました。", vbCritical
Exit Sub
End If
' 3. データ取得
sql = "SELECT TOP 100 * FROM SalesData ORDER BY SalesDate DESC"
Set rs = db.GetRecordset(sql)
If Not rs Is Nothing Then
' --- ここでデータ処理を実施 ---
Do While Not rs.EOF
' 処理...
rs.MoveNext
Loop
' ----------------------------
' 4. Recordsetの明示的な破棄(重要!)
rs.Close
Set rs = Nothing
End If
' 5. クラスの破棄(CloseConnectionが自動的に呼ばれる)
' プロシージャを抜ける際に、db変数のスコープが切れ、
' Class_Terminateが走り、メモリが完全に解放される。
Set db = Nothing
MsgBox "処理完了。メモリはクリーンに保たれました。", vbInformation
End Sub
5. 結論:エンジニアとして持つべき「重み」の意識
プロフェッショナルなコードとは、単に「動く」コードではない。「動いた後の状態を、いかに美しく保てるか」まで設計に組み込まれたコードのことだ。
ADOの接続プーリングという強力な武器を、制御不能なメモリ肥大化の凶器にしないために。
君が書く一行の `Set obj = Nothing` には、システムの寿命を延ばすための重みがある。
長時間稼働するシステムを設計する際は、常に自分に問いかけろ。
「このオブジェクトは、使い終わった後、本当にこの世から消滅しているか?」と。
