DAO Recordsetの「死」を制御せよ:メモリリークを根絶するオブジェクト破棄の極限設計
システムが稼働し続けて数ヶ月、あるいは数年。ある日突然、Accessアプリケーションが「メモリ不足」を吐いて沈黙する。あるいは、`.accdb`ファイルがロックされ、他のプロセスからアクセスできなくなる。
多くのエンジニアはこれを「Accessの限界」や「OSの仕様」として片付ける。だが、それは思考停止に過ぎない。真の原因は、コードの書き方、すなわちオブジェクトのライフサイクル管理の欠如にある。
本稿では、DAO(Data Access Objects)を扱う上で避けては通れない、メモリリークとリソースリークのメカニズムを解剖し、プロフェッショナルが実装すべき「堅牢な破棄パターン」を提示する。
1. アーキテクチャの深淵:なぜDAOは「漏れる」のか
まず、我々が扱っているDAOの本質を理解しなければならない。DAOは、ACE (Access Connectivity Engine) または旧来の JET エンジンと密接に結合した、非常にタイトなアーキテクチャである。
参照カウントとメモリの所在
VBAにおけるオブジェクト管理は「参照カウント方式」で行われる。オブジェクトに対する参照がゼロになったとき、初めてそのオブジェクトはメモリから解放される。
しかし、DAOの `Recordset` や `Database` オブジェクトは、単なるVBA上のメモリ領域に存在するのではない。それらはACEエンジンの内部メモリ空間(Unmanaged Memory)にリソースを確保し、ファイルシステムへの排他的ロックを保持している。
ここで致命的な問題が発生する。
VBAの変数(参照)がスコープを外れたとしても、何らかの理由で参照カウントがゼロにならなかった場合、VBAのメモリではなく、ACEエンジン側のリソースが「幽霊」のように残り続けるのだ。これが、いわゆる「メモリリーク」の正体である。
`CurrentDb` の罠
シニアエンジニアなら、以下のコードの危うさを即座に察知できるはずだ。
' 悪手:呼び出すたびに新しいDatabaseオブジェクトが生成される
Dim rs As DAO.Recordset
Set rs = CurrentDb.OpenRecordset("SELECT * FROM T_Orders")
`CurrentDb` は呼び出されるたびに、新しい `Database` オブジェクトのインスタンスを生成する。このインスタンスを明示的に `Set Nothing` しない限り、内部的な参照が蓄積され、エンジン側のメモリを食いつぶしていく。
2. 破棄の鉄則:`Close` と `Set Nothing` の決定的な違い
初心者は混同しがちだが、これらは全く異なる次元の操作である。
1. `rs.Close`
- 役割: データソースへの接続(カーソル)を閉じ、レコードセットとしての機能を停止させる。
- 状態: オブジェクト自体はまだメモリ上に存在し、VBAの変数(参照)は依然としてそのオブジェクトを指している。
2. `Set rs = Nothing`
- 役割: 変数からオブジェクトへの参照を切り離す。
- 状態: これにより参照カウントが減算され、カウントがゼロになった瞬間に、メモリおよびACEエンジン内のリソースが解放される。
「`Close` だけでは不十分であり、`Set Nothing` こそが解放の完遂である」。これを血肉に刻んでおく必要がある。
3. 実戦的実装:エラーハンドリングを包含した「完全破棄パターン」
メモリリークが発生する最大のシナリオは、「エラー発生による処理の中断」である。
通常のフローでは `Set Nothing` を通るコードであっても、実行中にエラーが発生して `On Error GoTo` でエラーハンドラへジャンプした場合、破棄処理はスキップされる。
これを防ぐには、「単一の出口(Single Exit Point)」設計を徹底しなければならない。
プロフェッショナル・テンプレート
以下に、私が設計思想として採用している、極めて堅牢なテンプレートを示す。
''' <summary>
''' 業務データの更新処理(メモリリーク防止実装版)
''' </summary>
Public Sub ExecuteSecureTransaction()
' 1. オブジェクトの宣言(初期化は行わない)
Dim db As DAO.Database
Dim rs As DAO.Recordset
Dim strSql As String
' エラーハンドリングの設定
On Error GoTo ErrorHandler
' 2. オブジェクトの初期化
' CurrentDbではなく、明示的に開いたDatabaseオブジェクトを使用することを推奨
Set db = DBEngine.Workspaces(0).OpenDatabase(CurrentProject.FullName)
strSql = "SELECT * FROM T_Inventory WHERE Status = 'Active'"
Set rs = db.OpenRecordset(strSql, dbOpenDynaset)
' --- メインロジック開始 ---
Do Until rs.EOF
' 何らかの複雑な計算や、外部API呼び出しを想定
' ここでエラーが発生しても、必ずExitSubへ誘導される
Debug.Print "Processing: " & rs!ItemName
rs.MoveNext
Loop
' --- メインロジック終了 ---
' 3. 正常終了・エラー終了共通の出口
ExitSub:
' 4. 破棄処理(逆順に破棄するのが鉄則)
' エラー発生時でも確実にここを通るように設計する
On Error Resume Next ' 破棄処理中のエラーによる二次エラーを防止
If Not rs Is Nothing Then
rs.Close
Set rs = Nothing
End If
If Not db Is Nothing Then
db.Close
Set db = Nothing
End If
' 参照カウントを確実にクリアするためのメモリ同期(必要に応じて)
' DoEvents ' 極めて重いループの直後には検討の余地あり
Exit Sub
' 5. エラーハンドラ
ErrorHandler:
' エラー情報のログ出力(実際の実装ではログファイルやテーブルへ)
Debug.Print "Error " & Err.Number & ": " & Err.Description
' エラーを上位に伝播させるか、あるいは適切に処理してExitSubへ
' ここではExitSubへ飛ばして確実にクリーンアップを行う
Resume ExitSub
End Sub
アーキテクトの視点:なぜ「逆順」か?
オブジェクトを破棄する際は、「作成した順序の逆」で行う。
`Recordset` は `Database` に依存している。親(Database)を先に破棄してしまうと、子(Recordset)が親のメモリ領域を参照しようとした際に、不正なメモリアクセス(Accessの強制終了)を引き起こすリスクがあるからだ。
4. 極限の最適化:システム間連携とレガシー保守への提言
大規模なシステム、あるいは外部のSQL Server等と連携する環境においては、以下の知見も考慮に含めるべきである。
- トランザクションの境界:
`db.BeginTrans` を使用する場合、エラー発生時に `db.Rollback` を行わなければ、データベースのロックが解放されない。`Rollback` もまた、オブジェクト破棄と同様に `ExitSub` 内で確実に実行されるべき「リソース管理」の一部である。
- Windows API との境界:
VBAから `Declare Function` を用いて Windows API を呼び出す際、API側で確保されたメモリ(`HeapAlloc` 等)がある場合、VBAの `Set Nothing` だけではそのメモリは解放されない。APIを使用するモジュールには、必ず対になる `Free` 処理をセットで実装せよ。
- レガシー環境の保守:
古い Access 2003/2007 等の環境では、メモリ管理の精度が現代より低い。このような環境では、大規模なループ処理の合間に `DoEvents` を挟み、OS側に制御を戻してメモリの再配置を促す手法も、一つの「妥協ある最適解」となり得る。
結論
DAOにおけるメモリ管理とは、単なる「お作法」ではない。それは、アプリケーションの寿命を決定づける、エンジニアリングの根幹である。
`Set Nothing` を、単なるコードの末尾に添える記号として扱うな。それは、オブジェクトの魂を、システムから確実に解き放つための「儀式」である。この意識の差が、安定したシステムを構築できるシニアエンジニアと、デバッグ地獄に陥るジュニアエンジニアを分かつ。
