【Access VBA】DAO.Recordsetの「EOF」と「BOF」:空レコードセットによる実行時エラーを撲滅する
レガシーシステムの保守、あるいは極限まで最適化されたオンプレミス型業務アプリケーションの現場において、Access VBAの挙動を完全に掌握することは、シニアエンジニアにとっての必須教養である。
とりわけ、データベースとの対話において避けて通れないのが `DAO.Recordset` の操作だ。
「検索結果が0件だったために、後続の処理で実行時エラーが発生した」――この極めてプリミティブで、かつ現場の信頼性を一瞬で崩壊させるバグを、我々は幾度となく目撃してきた。
今回は、`EOF` と `BOF` の真の挙動メカニズム、そして空のレコードセットが引き起こす罠を完全に無効化する「防御的プログラミング」の極限を提示する。
—
1. なぜ「空のレコードセット」は殺人的なのか
データベースからデータを取得する際、開発者の脳内には「データが存在する世界」しか描かれていないことが多い。
以下のようなコードを書いた瞬間、そのプログラムはシステム障害の爆弾を抱えることになる。
Dim db As DAO.Database
Dim rs As DAO.Recordset
Set db = CurrentDb
Set rs = db.OpenRecordset(“SELECT FROM T_Employees WHERE Department = ‘NotExists'”, dbOpenSnapshot)
‘ いきなりフィールドにアクセスする
Debug.Print rs!EmployeeName ‘ ← ここで容赦なく実行時エラー(またはNull参照の悲劇)が発生する
rs.Close
Set rs = Nothing
Set db = Nothing
`OpenRecordset` メソッドは、SQLの実行結果が0件であっても、通常はエラーを返さない。
返されるのは「レコードが1件も存在しない、しかし有効なインスタンスとしてのRecordset」である。この状態において、ポインタはどこをも指すことができず、`EOF`(End Of File)も `BOF`(Beginning Of File)も同時に `True` というパラドックスめいた状態に陥る。
この状態で `rs!FieldName` にアクセスしようとすると、Accessエンジンはパニックを起こし、実行時エラーを吐き出すか、最悪の場合は暗黙のNull汚染を引き起こす。
—
2. `EOF` と `BOF` のメモリポインタ的解釈
`EOF` と `BOF` を単なる「おまじないの判定プロパティ」と考えているうちは、三流のプログラマに過ぎない。
アーキテクトの視点では、これらは「レコードセットというメモリ空間内における仮想カーソルの現在地を示すフラグ」である。
- `BOF` (Beginning Of File): カーソルが先頭レコードよりも「さらに前」にある時 `True`
- 湿気た `EOF` (End Of File): カーソルが最終レコードよりも「さらに後ろ」にある時 `True`
レコードが0件の時、レコードセットが開かれた瞬間にカーソルは行き場を失い、先頭にも後端にもない(あるいはその両方を超越している)ため、DAOの仕様により `EOF` と `BOF` の双方が `-1 (True)` を返す。
この物理法則を理解していれば、安全な航海(データ走査)を行うための条件式自ずと導き出せる。
—
3. 実装パターン:極限まで洗練された防御的プログラミング
実務の現場では、単にエラーを防ぐだけでなく、メモリリークの防止(オブジェクトの明示的解放)、そしてパフォーマンスへの配慮が求められる。
以下のコードは、数百万件規模のデータナレッジを持つ基幹システムでも耐えうる、完璧な構造を持つプロトタイプである。
‘ ==============================================================================
‘ 担当者マスタから安全にレコードを取得するプロシージャ
‘ ==============================================================================
Public Sub ProcessEmployeeDataSafe(ByVal targetDept As String)
Dim db As DAO.Database
Dim rs As DAO.Recordset
Dim strSQL As String
On Error GoTo ErrorHandler
‘ データベース参照の取得(CurrentDbの乱用を防ぐためローカル変数に格納)
Set db = CurrentDb()
‘ パフォーマンスを考慮し、変更しないデータは Snapshot で取得
strSQL = “SELECT EmployeeID, EmployeeName FROM T_Employees WHERE Department = ‘” & CleanSQL(targetDept) & “‘”
Set rs = db.OpenRecordset(strSQL, dbOpenSnapshot, dbForwardOnly)
‘ 【極限の防御】EOF と BOF の両方をチェックし、データが存在するか厳格に検証する
If rs.BOF And rs.EOF Then
‘ 0件の場合の処理(ログ出力や早期脱出)
MsgBox “対象データが存在しません。”, vbInformation, “システム通知”
GoTo Cleanup
End If
‘ データを安全に走査する
Do While Not rs.EOF
‘ ビジネスロジックをここに記述
Debug.Print “ID: ” & rs!EmployeeID & “, Name: ” & rs!EmployeeName
rs.MoveNext
Loop
Cleanup:
‘ 【鉄則】オブジェクトの明示的解放(メモリ最適化)
If Not rs Is Nothing Then
rs.Close
Set rs = Nothing
End If
Set db = Nothing
Exit Sub
ErrorHandler:
‘ 予期せぬ例外処理
MsgBox “致命的なエラーが発生しました: ” & Err.Description, vbCritical, “エラー”
Resume Cleanup
End Sub
‘ 簡易的なSQLインジェクション対策(必要に応じてParamQueryに昇格させること)
Private Function CleanSQL(ByVal inputVal As String) As String
CleanSQL = Replace(inputVal, “‘”, “””)
End Function
アーキテクトのこだわりポイント
1. `dbForwardOnly` の採用
もしレコードを一方通行でしか読まないのであれば、`dbForwardOnly` オプションを付与せよ。Jet/ACEエンジンは余計な双方向キャッシュを生成せず、メモリ消費量を最小限に抑えつつ高速化を実現する。
2. `CurrentDb` のキャッシュ
`CurrentDb` メソッドは呼び出すたびに内部で新しいDatabaseオブジェクトのインスタンスを生成する。同一プロシージャ内では変数に一度だけ格納し、リソースの無駄撃ちを避けること。
3. 確実なオブジェクト破棄(Cleanupブロック)
VBAのガベージコレクションを信用するな。特にエラー発生時にRecordsetが開いたまま放置されると、Accessのロックファイル(.laccdb)が解放されず、他のユーザーのアクセスを阻害する原因となる。
—
4. まとめ:プロのコードとアマチュアのコードの境界線
「データがあるはずだ」という前提で書かれたコードは、運用フェーズに入った瞬間に必ずシステムを殺す。
`EOF` と `BOF` の状態を制することは、Access VBAにおけるデータベース制御の「作法」であり、プロとアマを分ける明確な境界線である。
いかなる極限のレガシー環境であっても、常に「データは0件かもしれない」という疑念を持ち、堅牢なガードを構築すること。それこそが、システムアーキテクトに課された揺るぎなき責務である。
