Access VBAを掌握する極限の知見:Recordsetのライフサイクルを支配せよ
「動いているから、これでいい」
もし君がそう思っているのなら、君が作っているツールは、いつか必ず牙を剥く。
大規模なデータを扱うようになった瞬間、あるいは長時間連続して稼働させた時、Accessは突如として動作が重くなり、最悪の場合は「データベースの破損」という、開発者にとって最も忌むべき事態を引き起こす。その引き金となる原因の多くは、コードのロジックミスではない。オブジェクトのライフサイクル、すなわちRecordsetの解放タイミングの設計ミスだ。
今回は、多くの開発者が「なんとなく」で済ませているDAO Recordsetのクローズ戦略について、アーキテクトの視点からその真理を解説する。
1. なぜ「プロシージャの終了」を信じてはいけないのか
多くの初級者は、プロシージャ内で `Dim rs As DAO.Recordset` と宣言し、処理が終われば変数がいずれ破棄されるから大丈夫だと誤解している。
しかし、VBAのメモリ管理(ガベージコレクション)は、C++のように厳密でもなければ、Javaのように完全に自動的でもない。VBAのオブジェクト変数は、参照カウントがゼロになった時に解放されるが、DAOのRecordsetは、メモリ上の変数だけでなく、背後のデータベースエンジン(ACE/Jet)に対して「ファイルロック」や「カーソル」という実体的なリソースを確保している。
プロシージャが終了しても、明示的に `.Close` を呼ばない限り、データベースエンジン側は「まだこのレコードセットは使われている可能性がある」と判断し、リソースを掴み続けることがある。これが、`.laccdb` ファイル(ロックファイル)が消えない原因であり、マルチユーザー環境でのデッドロックの温床である。
鉄則:オブジェクトの寿命は、言語の仕様に任せるな。エンジニアの意志で制御せよ。
2. スコープによる戦略的使い分け
Recordsetをどこで宣言するかによって、クローズすべきタイミングは明確に異なる。これを混同することが、バグの入り口だ。
A. プロシージャ内変数(Local Scope)
特定の計算や、一時的なデータ抽出に使用する場合。
- 寿命: そのプロシージャの実行中のみ。
- クローズのタイミング: プロシージャの最後。ただし、エラーが発生した場合でも必ずクローズされる構造にしなければならない。
B. フォーム・モジュール変数(Module Scope)
フォームにデータをバインドしたり、フォームを開いている間、継続的にレコードを走査したりする場合。
- 寿命: フォームが開いている間。
- クローズのタイミング: フォームの `Close` イベント、または `Unload` イベント。ここで解放を忘れると、フォームを閉じた後もメモリにオブジェクトが残り続け、次に同じフォームを開こうとした際に「既にオブジェクトが開いています」といったエラーを誘発する。
3. プロダクション品質のコード・パターン
では、実務でそのまま使える「堅牢な設計」はどう書くべきか。単なる `rs.Close` ではない。エラーハンドリングを含めた、「確実な破棄(Deterministic Destruction)」を実装する。
パターン1:プロシージャ内での安全な解放
エラーが発生しても、必ずリソースを解放するための `On Error GoTo` 構造だ。
''' <summary>
''' 特定の条件に基づいてデータを抽出し、ログを出力する。
''' </summary>
Public Sub ProcessDataSafely()
Dim db As DAO.Database
Dim rs As DAO.Recordset
' エラーハンドリングの開始
On Error GoTo ErrorHandler
Set db = CurrentDb
' SQLの実行
Set rs = db.OpenRecordset("SELECT * FROM T_Transactions WHERE Status = 'Pending'", dbOpenSnapshot)
' データ処理のループ
Do Until rs.EOF
Debug.Print "Processing: " & rs!TransactionID
' --- ここに複雑なロジックが入る ---
rs.MoveNext
Loop
CleanUp:
' --- 終了処理(正常終了・エラー終了の両方からここへ飛ぶ) ---
' 1. Recordsetのクローズと解放
If Not rs Is Nothing Then
On Error Resume Next ' クローズ自体でエラーが出ても無視して進む
rs.Close
Set rs = Nothing
On Error GoTo 0
End If
' 2. Databaseオブジェクトの解放
If Not db Is Nothing Then
Set db = Nothing
End If
Exit Sub
ErrorHandler:
' エラーログの記録(実務ではログテーブル等へ)
MsgBox "エラーが発生しました: " & Err.Description, vbCritical, "Execution Error"
Resume CleanUp ' 必ずCleanUpへ飛ばしてリソースを解放する
End Sub
パターン2:フォームレベルでのライフサイクル管理
フォームのメモリリークを防ぐための、クラス設計に近いアプローチだ。
Option Compare Database
Option Explicit
' モジュールレベル変数として保持
Private m_rs As DAO.Recordset
''' <summary>
''' フォーム読み込み時にRecordsetをセットアップ
''' </summary>
Private Sub Form_Load()
On Error GoTo ErrorHandler
' フォームが開いている間、常に参照できるRecordset
Set m_rs = CurrentDb.OpenRecordset("SELECT * FROM T_MasterData", dbOpenDynaset)
' データの表示処理など
Exit Sub
ErrorHandler:
MsgBox "Load Error: " & Err.Description
End Sub
''' <summary>
''' フォームが閉じられる際に、確実にリソースを解放する
''' </summary>
Private Sub Form_Close()
' フォームが閉じるとき、Recordsetを確実に殺す
Call ReleaseResources
End Sub
''' <summary>
''' リソース解放の責務を分離したプライベートメソッド
''' </summary>
Private Sub ReleaseResources()
On Error Resume Next
If Not m_rs Is Nothing Then
m_rs.Close
Set m_rs = Nothing
End If
Debug.Print "Recordset released successfully."
End Sub
4. アーキテクトからのアドバイス:設計の極意
最後に、君たちが設計段階で意識すべき「重み」を伝えておく。
1. `Set rs = Nothing` は `rs.Close` の後に必ず書け
`.Close` は「通信(接続)を断つ」行為であり、`Set = Nothing` は「変数(ポインタ)を消す」行為だ。この両方が揃って初めて、メモリ空間は完全にクリーンになる。
2. `dbOpenSnapshot` を活用せよ
読み取り専用の処理であれば、`dbOpenDynaset` ではなく `dbOpenSnapshot` を使え。これはメモリ消費が少なく、ロックの競合も最小限に抑えられる。パフォーマンスと安全性、両面において優れている。
3. 「エラー時こそ、解放の儀式」を行え
`On Error GoTo ErrorHandler` を書いたなら、そのハンドラの中で「何もしない」ことは許されない。必ず `CleanUp` ラベルへと誘導し、リソース解放のルーチンを通るように設計せよ。
コードの美しさは、正常に動くことではなく、「異常事態において、いかにエレガントに立ち去れるか」で決まる。
君の書くコードが、堅牢で、美しく、そしてプロフェッショナルであることを期待している。
