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

スポンサーリンク

DAO.Recordsetの「EOF」と「BOF」:空レコードセットによる実行時エラーを撲滅する

開発現場でよく見かける光景がある。「データが存在するはず」という性善説に基づき、SQLの結果を無造作にレコードセットへ格納し、そのまま `.Fields(0).Value` を叩いて盛大に轟沈するバグだ。

「レコードが見つかりませんでした」というエラーで業務システムが止まる。ユーザーからの冷ややかな視線。その原因の9割は、`DAO.Recordset` のライフサイクル、特に `EOF` (End Of File) と `BOF` (Beginning Of File) の概念を軽視した設計ミスにある。

今回は、Access VBAにおけるデータベース操作の急所であり、堅牢なシステムを作るための絶対不可欠な防御的プログラミング手法を、チーフアーキテクトの視点から授けよう。

1. なぜ「空のレコードセット」でエラーが起きるのか?

Access VBAでデータベースにアクセスする際、私たちは主にDAO(Data Access Objects)の `CurrentDb.OpenRecordset` を用いる。
SQLの条件に一致するレコードが1件もヒットしなかった時、Accessは何を返すか?

「Null」ではなく、「レコードが1件も存在しない(空の)Recordsetオブジェクト」を返すのだ。

この時、Recordsetの内部ポインタはどこを指しているか?
答えは、`EOF` も `BOF` も同時に `True` という、宙に浮いた状態である。

この状態で `.MoveNext` を呼んだり、存在しないレコードのフィールド値にアクセスしようとすると、VBAは容赦なく実行時エラー(エラー3021: 「カレント レコードがありません。」など)を吐き出す。これが、初心者が最初に踏む最大の地雷原である。

2. 破綻するコード vs 堅牢なコード

まずは、世にはびこる「危険なコード」と、プロフェッショナルが書く「堅牢なコード」を見比べてほしい。

【アンチパターン】エラーを呼び込む脆弱なコード

Sub GetCustomerName_Bad(CustomerID As Long)
Dim db As DAO.Database
Dim rs As DAO.Recordset

Set db = CurrentDb
Set rs = db.OpenRecordset(“SELECT CustomerName FROM T_Customers WHERE CustomerID = ” & CustomerID)

‘ ⚠️ 危険:データがない場合、ここで即死する
MsgBox “顧客名: ” & rs.Fields(“CustomerName”).Value

rs.Close
Set rs = Nothing
Set db = Nothing
End Sub

このコードは、指定したIDの顧客が物理的に存在しない瞬間、システム停止という致命傷を負う。

【プロダクションコード】EOF/BOFを制する防御的プログラミング

Sub GetCustomerName_Good(CustomerID As Long)
Dim db As DAO.Database
Dim rs As DAO.Recordset

On Error GoTo ErrorHandler

Set db = CurrentDb
Set rs = db.OpenRecordset(“SELECT CustomerName FROM T_Customers WHERE CustomerID = ” & CustomerID, dbOpenSnapshot)

‘ 🛡️ 鉄壁のガード:データが存在するか厳密にチェックする
If rs.EOF And rs.BOF Then
MsgBox “該当する顧客データは見つかりませんでした。”, vbExclamation, “確認”
GoTo Cleanup
End If

‘ 安全にデータを取得
MsgBox “顧客名: ” & rs.Fields(“CustomerName”).Value, vbInformation, “処理成功”

Cleanup:
‘ 資源の解放(メモリリークの撲滅)
If Not rs Is Nothing Then rs.Close
Set rs = Nothing
Set db = Nothing
Exit Sub

ErrorHandler:
MsgBox “予期せぬエラーが発生しました: ” & Err.Description, vbCritical
Resume Cleanup
End Sub

3. EOF と BOF のメカニズムを完璧に理解する

`EOF` と `BOF` の状態を、アーキテクトとして正確に定義しておこう。

| 状態 | 意味 | ポインタの位置 |
| :— | :— | :— |
| `EOF = False`, `BOF = False` | 有効なレコードを指している | レコードの途中 |
| `EOF = True` | 最後のレコードよりも後ろにいる | 終端より先(データなし含む) |
| `BOF = True` | 最初のレコードよりも前にいる | 先頭より前(データなし含む) |
| `EOF = True` 且つ `BOF = True` | レコードが1件も存在しない | 完全な空っぽ |

したがって、データを処理する前の定型イディオムとしては、以下のように記述するのが鉄則となる。

If rs.EOF And rs.BOF Then
‘ データが0件の場合の処理
Else
‘ データが存在する場合の処理(ループまたは先頭行の処理)
End If

4. 複数件のレコードを処理する場合の正しいイディオム

単票形式の取得だけでなく、複数件のレコードをループ処理(走査)する場合も、`EOF` の判定が命綱になる。

Sub ProcessActiveOrders()
Dim db As DAO.Database
Dim rs As DAO.Recordset

Set db = CurrentDb
Set rs = db.OpenRecordset(“SELECT FROM T_Orders WHERE Status = ‘未処理'”, dbOpenSnapshot)

‘ 1. まず空チェック
If rs.EOF And rs.BOF Then
MsgBox “処理対象のデータはありません。”, vbInformation
GoTo Cleanup
End If

‘ 2. 先頭から末尾まで安全にループ
Do While Not rs.EOF
‘ — ここにビジネスロジックを記述 —
Debug.Print “処理中ID: ” & rs.Fields(“OrderID”).Value
‘ ———————————-

rs.MoveNext ‘ 次のレコードへポインタを移動
Loop

Cleanup:
If Not rs Is Nothing Then rs.Close
Set rs = Nothing
Set db = Nothing
End Sub

ここで `Do While Not rs.EOF` を使う理由は、データが存在することが最初の `If rs.EOF And rs.BOF Then` で担保されているからこそ、安全にループを回せるという美しい依存関係にある。

5. アーキテクトからの実践的なアドバイス

1. `dbOpenSnapshot` を常に意識せよ
データを更新する目的がない参照系のクエリ(集計や画面表示用)では、必ず第2引数に `dbOpenSnapshot` を指定すること。不要なロックを行わず、メモリ消費も抑えられるため、パフォーマンスが劇的に向上する。
2. オブジェクトの解放(Cleanup)を怠るな
VBAはガベージコレクションの挙動が緩慢である。プロシージャの抜け際で `rs.Close` と `Set rs = Nothing` を徹底しないと、Accessのデータベースファイルが肥大化し、最悪の場合はファイル破損を引き起こす。
3. 「エラーが起きないこと」ではなく「エラーを起きなくさせる設計」を
`On Error Resume Next` でエラーを握りつぶすような愚行は断じて避けてほしい。`EOF` と `BOF` を正しくコントロールし、制御フローの中でバグを完全にルーティングする――これが、プロの書くコードの美学である。

明日から、いや、今書いているそのコードから、すべての `OpenRecordset` の直下に `If rs.EOF And rs.BOF` の盾を構えてほしい。それだけで、あなたの作る業務システムの信頼性は、圧倒的な高みへと到達するはずだ。

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