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

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

長時間稼働する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` には、システムの寿命を延ばすための重みがある。

長時間稼働するシステムを設計する際は、常に自分に問いかけろ。
「このオブジェクトは、使い終わった後、本当にこの世から消滅しているか?」と。

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