【実務・中級編】DAOのRecordset操作で発生する「メモリリーク」を未然に防ぐ:オブジェクト破棄の完全ガイド – Access VBA解析バイブル

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

リソース解放の美学:DAO Recordsetの「メモリリーク」を根絶するアーキテクチャ設計

業務システムを構築する際、多くの開発者が「ロジックの正しさ」にのみ執着し、致命的な落とし穴を見落としている。それが「オブジェクトのライフサイクル管理」だ。

「動いているから問題ない」という言葉は、プロフェッショナルにとっては敗北宣言に等しい。特に、Access VBAにおいてDAO(Data Access Objects)を扱う際、適切にリソースを解放しないコードは、長時間稼働させた際にメモリを食いつぶし、最終的にはAccessそのものをクラッシュさせる「時限爆弾」となる。

今日は、設計の甘さが招くメモリリークのメカニズムを解剖し、プロダクション環境に耐えうる「堅牢なオブジェクト破棄の作法」を伝授する。

なぜ「Set Nothing」だけでは不十分なのか

初心者はよくこう言う。「最後に `Set rs = Nothing` と書いているから大丈夫です」と。
断言しよう。それは極めて危険な誤解だ。

VBAにおけるオブジェクトは、COM(Component Object Model)という仕組みに基づいた「参照カウント」によって管理されている。オブジェクトがメモリ上に留まるのは、そのオブジェクトを指し示している変数が1つ以上存在する場合だ。

問題は、「エラーが発生したとき」に起こる。

破綻するコードの典型例

以下のコードを見てほしい。一見、正しくオブジェクトを解放しているように見える。

' ❌ 脆弱な設計:エラー時にリソースがリークする
Sub BadExample()
    Dim db As DAO.Database
    Dim rs As DAO.Recordset
    
    Set db = CurrentDb
    Set rs = db.OpenRecordset("SELECT * FROM HeavyTable", dbOpenSnapshot)
    
    ' --- 何らかの複雑なロジック ---
    ' ここで予期せぬエラー(型不一致、データ欠損など)が発生すると...
    Dim x As Integer
    x = 1 / 0 ' ゼロ除算エラー発生!
    ' ---------------------------

    rs.Close
    Set rs = Nothing ' ← ここに到達せず、rsはメモリ上に「幽霊」として残り続ける
    db.Close
    Set db = Nothing
End Sub

エラーが発生した瞬間、プログラムの実行権限は `Err_Handler`(エラーハンドラ)へジャンプする。その結果、`rs.Close` や `Set rs = Nothing` という「解放の儀式」はスキップされる。

この「スキップされたオブジェクト」は、参照が残ったままメモリを占有し続ける。これが積み重なることで、メモリリークが発生し、システムは死に至るのだ。

プロフェッショナルが採用する「クリーンアップ・パターン」

堅牢なシステムを設計するアーキテクトは、「正常終了時」と「異常終了時」の出口を一本化する。 これを「クリーンアップ・パターン」と呼ぶ。

設計の鉄則はこうだ:
1. エラーが発生しても、必ず「解放処理専用のセクション」を通るように設計せよ。
2. `Resume CleanUp` を使い、エラーハンドラから解放処理へ合流せよ。

完璧な実装テンプレート

以下に、実務でそのまま利用できる、極めて堅牢な構造を示す。これをテンプレートとして脳に刻んでほしい。

''' <summary>
''' 業務データ処理の堅牢なテンプレート
''' </summary>
Public Sub RobustDataProcessing()
    ' 1. オブジェクト変数の宣言(初期値はNothingであることが保証される)
    Dim db As DAO.Database
    Dim rs As DAO.Recordset
    Dim strSql As String
    
    ' エラーハンドリングの設定
    On Error GoTo Err_Handler

    ' 2. オブジェクトの初期化
    Set db = CurrentDb
    strSql = "SELECT * FROM T_Transaction WHERE Status = 'Pending'"
    Set rs = db.OpenRecordset(strSql, dbOpenSnapshot)

    ' 3. メインロジック
    ' ---------------------------------------------------------
    Do Until rs.EOF
        ' ここで複雑な計算や外部API連携を行う
        ' 万が一ここでエラーが起きても、確実にクリーンアップへ向かう
        Debug.Print rs!TransactionID
        rs.MoveNext
    Loop
    ' ---------------------------------------------------------

' --- 正常終了時もここを通る ---
CleanUp:
    ' 4. オブジェクトの破棄(逆順で解放するのが鉄則)
    ' エラーが発生していても、ここへ Resume CleanUp によって到達する
    On Error Resume Next ' 解放処理中のエラーによる二次災害を防ぐ
    
    If Not rs Is Nothing Then
        rs.Close
        Set rs = Nothing
    End If
    
    If Not db Is Nothing Then
        ' CurrentDbを使用している場合はClose不要だが、
        ' 外部DB接続の場合は必ずCloseが必要
        Set db = Nothing
    End If
    
    Exit Sub

' 5. エラーハンドリング
Err_Handler:
    ' エラーログの出力(実務ではログファイルやテーブルへ記録せよ)
    MsgBox "致命的なエラーが発生しました。" & vbCrLf & _
           "Error No: " & Err.Number & vbCrLf & _
           "Description: " & Err.Description, vbCritical, "System Error"
    
    ' 重要:エラーを報告した後、必ずクリーンアップ・セクションへ合流させる
    Resume CleanUp
End Sub

アーキテクトの視点:さらなる高みへ

このパターンをマスターした君に、さらに踏み込んだ知見を授けよう。

1. 解放の「逆順」原則

オブジェクトのライフサイクルには依存関係がある。`Recordset` は `Database` に依存している。したがって、破棄する際は必ず 「子(Recordset)から先に、親(Database)を後から」 解放せよ。親を先に `Nothing` にしてしまうと、子の解放プロセスが宙に浮き、制御不能なメモリリークを招く可能性がある。

2. `On Error Resume Next` の正しい使い方

上記の `CleanUp` セクション内で `On Error Resume Next` を使用していることに気づいたか?
これは「サボり」ではない。「解放処理そのものが失敗したときに、エラーハンドラが無限ループしたり、エラーを握りつぶしてプロセスを停止させたりするのを防ぐ」 ための、極めて理にかなった防衛策だ。

3. DAOとADOの境界線

もし君が、外部のSQL ServerへADO(ActiveX Data Objects)で接続する場合、管理の重要性はさらに跳ね上がる。ADOの `Connection` オブジェクトは、ネットワークリソースやサーバー側のセッションを直接占有する。DAOのメモリリークは「ローカルのクラッシュ」で済むが、ADOの接続リークは「サーバー側のセッション枯渇」を招き、全社的なシステムダウンを引き起こす。

結論

「動くコード」を書くのは初級者だ。「壊れないコード」を書くのが中級者。そして、「リソースの行方まで完全に制御下に置いたコード」を書くのが、我々プロフェッショナルだ。

メモリリークを防ぐことは、単なるテクニックではない。それは、ユーザーが安心して使い続けられる「信頼」を設計することと同義である。

次にコードを書くときは、自分に問いかけてほしい。
「もしここでエラーが起きたら、このオブジェクトは成仏できるか?」 と。

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