【テクニカル・上級編】【初心者】DAO.Recordsetの「EOF」と「BOF」:空レコードセットによる実行時エラーを撲滅する – Access VBA解析バイブル

スポンサーリンク

【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件かもしれない」という疑念を持ち、堅牢なガードを構築すること。それこそが、システムアーキテクトに課された揺るぎなき責務である。

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