この記事で分かること
- 実行時エラー3021が発生する根本的な原因とBOF/EOFの概念
- レコードセットが空の状態でデータを参照しようとしたときの挙動
- エラーを未然に防ぐためのBOF・EOFの適切な条件分岐コード
- 実務でよく使われるDAOとADOにおけるコードの記述例と違い
実行時エラー3021「BOFとEOFのいずれかが真になっているか、または現在のレコードが削除されています」とは
Microsoft Accessでデータベースの開発や運用(特にVBAを用いたプログラム処理)を行っている際、頻繁に遭遇するエラーの一つが「実行時エラー 3021」です。このエラーの正式なメッセージは、「BOFとEOFのいずれかが真になっているか、または現在のレコードが削除されています。要求された操作には、現在のレコードが必要です。」というものです。初めてこのメッセージを見た方は、難解なシステム用語が並んでいるため戸惑ってしまうかもしれません。
このエラーの本質を理解するには、まずVBAでデータを操作する際に使われる「レコードセット(Recordset)」という仮想的なデータの表と、それを指し示す「カレントレコード(現在参照している行)」の仕組み、そして「BOF」と「EOF」というプロパティについて理解する必要があります。
BOFは「Beginning of File(ファイルの先頭)」、EOFは「End of File(ファイルの末尾)」の略称です。これらは、レコードセット内における現在の「カーソルの位置」を示すプロパティであり、以下のような状態を表します。
| プロパティと状態 | 具体的な意味 | カレントレコードの有無 | エラー3021のリスク |
|---|---|---|---|
| BOF = True | レコードセットの先頭行よりも前にカーソルがある状態 | なし | 極めて高い(操作不可) |
| EOF = True | レコードセットの最終行よりも後ろにカーソルがある状態 | なし | 極めて高い(操作不可) |
| BOF = True かつ EOF = True | レコードセットの中にデータが1件も存在しない状態(空のテーブル/クエリ結果) | なし | 最大(アクセスした瞬間にエラー) |
このように、BOFまたはEOFが「True」になっているとき、Accessは「現在どのレコード(行)を処理すればよいのか分からない」状態になります。この状態であるにもかかわらず、プログラムがデータの読み込みや書き込み、編集などの操作を行おうとした瞬間に、実行時エラー3021が引き起こされます。
エラー3021が発生する主な原因と仕組み
VBAプログラムがこのエラーを発生させるのには、いくつかの明確なシナリオが存在します。原因を正しく把握することで、デバッグや予防対策が非常にスムーズになります。主な原因は以下の3つに集約されます。
原因1:条件に合致するレコードが1件もない(検索結果が空)
実務で最も多く見られる原因がこれです。例えば、ユーザーが画面上で入力した条件をもとにSQL文を組み立て、OpenRecordsetでレコードセットを開いたとします。しかし、入力された条件に一致するデータがデータベース内に1件も登録されていなかった場合、取得されるレコードセットは「レコード件数0件」の空っぽな状態になります。
この「空のレコードセット」が開かれた瞬間、カレントレコード(指し示す行)はどこにも存在しないため、BOFとEOFが同時に「True」になります。プログラム側でデータが存在しない可能性を考慮せずに、直後に「レコードから値を取り出す処理」を記述していると、瞬時にエラー3021が作動します。
原因2:ループ処理内でEOFを超えて移動してしまった
レコードセット内のデータを1件目から順番に処理していく際、VBAではDo Until rs.EOFのようなループ構文を使用します。これは「EOF(末尾)に達するまで処理を繰り返す」という指示です。
通常であれば、ループの最後でrs.MoveNextを記述して次のレコードにカーソルを進めますが、以下のようなミスがあるとEOFを通り越してエラーが発生します。
- ループ内の条件分岐(If文など)の記述ミスにより、
rs.MoveNextが1回のループの中で複数回実行されてしまい、末尾の行を飛び越えてしまう。 - 単純に
rs.MoveNextの記述を忘れてしまい、同じレコードを無限に処理しようとするか、カーソル位置の制御が狂ってしまう。
原因3:他ユーザーや別処理によってレコードが削除された
マルチユーザー環境(複数の職員が同時に同じAccessデータベースを操作する環境)では、競合が原因でエラーが発生することもあります。自端末のVBAプログラムが特定のレコードを取得し、編集処理を行おうとするまさにその直前に、別のユーザーがそのレコードをデータベースから物理的に削除してしまった場合です。
プログラムが「カレントレコードが存在する」と認識して処理を進めても、実態となるデータが削除されているため、Accessは「現在のレコードが削除されています」としてエラー3021を返します。
【対策】BOF・EOF의 判定を入れる正しいVBAコードの書き方
実行時エラー3021を完全に防ぐための最も効果的な対策は、「レコードセットを操作する前に、データが1件以上存在するかを必ずチェックする」ことです。具体的には、BOFとEOFが共にTrueではない(=データが少なくとも1件は存在する)ことを確認する条件分岐を記述します。
ここでは、実務でよく使われる「DAO(Data Access Objects)」と「ADO(ActiveX Data Objects)」の2つのパターンについて、正しい対策コードの記述例を紹介します。
1. DAOでの正しいコードの書き方(Access標準)
Access内のローカルテーブルやリンクテーブルを操作する場合に最も一般的に使用されるDAOでの記述例です。
Sub SafeRecordsetDAO()
Dim db As DAO.Database
Dim rs As DAO.Recordset
Dim strSQL As String
Set db = CurrentDb
' 顧客ID「9999」を検索する(データが存在しない可能性がある条件)
strSQL = "SELECT 顧客ID, 顧客名 FROM T_顧客マスタ WHERE 顧客ID = 9999"
Set rs = db.OpenRecordset(strSQL, dbOpenDynaset)
' 【重要】BOFとEOFが同時にTrueでない(=データがある)場合のみ処理を行う
If Not (rs.BOF And rs.EOF) Then
' 安全にレコードを処理
MsgBox "顧客名: " & rs!顧客名, vbInformation, "処理成功"
Else
' データがない場合の安全な代替処理
MsgBox "指定された顧客データは見つかりませんでした。", vbExclamation, "データなし"
End If
' 後片付け(クローズ処理とメモリ解放)
rs.Close
Set rs = Nothing
Set db = Nothing
End Sub
上記のコードでは、If Not (rs.BOF And rs.EOF)という判定を挟むことで、もし検索結果が0件であっても、エラーを出さずに安全に「データなし」の分岐(Else側)へ処理を誘導しています。これにより、実行時エラー3021を完全にシャットアウトできます。
2. ADOでの正しいコードの書き方(外部DB連携時)
SQL ServerやOracleなどの外部システムと連携する際に使われるADOでも、基本的な対策アプローチは全く同じです。
Sub SafeRecordsetADO()
Dim conn As ADODB.Connection
Dim rs As ADODB.Recordset
Dim strSQL As String
' 現在のデータベース接続を使用
Set conn = CurrentProject.Connection
strSQL = "SELECT 商品コード, 商品名 FROM T_商品マスタ WHERE 商品コード = 'X-123'"
Set rs = New ADODB.Recordset
rs.Open strSQL, conn, adOpenStatic, adLockReadOnly
' 【重要】ADOでも同様に存在チェックを行う
If Not (rs.BOF And rs.EOF) Then
MsgBox "商品名: " & rs!商品名, vbInformation, "処理成功"
Else
MsgBox "該当する商品は登録されていません。", vbExclamation, "データなし"
End If
' 後片付け
rs.Close
Set rs = Nothing
Set conn = Nothing
End Sub
このように、DAOとADOのどちらを使用する場合でも、レコードセットを開いた直後に「BOFとEOFのチェック」を行う癖をつけておくことが、プログラミングにおける鉄則です。
レコードが存在しない場合の処理方法と実務での注意点
実行時エラー3021を発生させないための基本的なコードの書き方を理解した上で、さらに実務における開発・運用の観点から押さえておくべき重要な注意点を3つ解説します。
注意点1:RecordCountプロパティの使い方と誤解に注意する
データが存在するかどうかを調べる際、If rs.RecordCount = 0 Thenという判定方法を思い浮かべる方も多いでしょう。確かに、レコード件数が0であればデータは存在しません。しかし、DAOの仕様上、RecordCountには以下のような注意が必要です。
DAOのダイナセット形式などでレコードセットを開いた直後は、たとえレコードが複数存在していたとしても、RecordCountの値は「1」にしかなりません(Accessがパフォーマンスを最優先し、まずは最初の1件分しか件数を読み込まないためです)。
ただし、データが「0件」の場合は正しく「0」を返します。そのため、0件かどうかの判定にRecordCountを利用することは可能ですが、確実なデータ件数を取得したい場合は、事前にrs.MoveLast(最後のレコードに移動する)を実行して全件を認識させる必要があります。
しかし、データが0件の状態でrs.MoveLastを実行してしまうと、それ自体が「実行時エラー3021」を発生させる引き金になります。したがって、以下のような順番で記述するのが実務でのセオリーです。
If Not (rs.BOF And rs.EOF) Then
rs.MoveLast ' 安全に末尾へ移動して件数をカウント
MsgBox "該当件数は " & rs.RecordCount & " 件です。"
Else
MsgBox "データは0件です。"
End If
注意点2:DLookup関数やNz関数との適切な使い分け
単一のデータをテーブルから検索して取得したいだけであれば、わざわざRecordsetを開かずに「DLookup関数」を使用する方が記述がシンプルで済む場合があります。
ただし、DLookup関数も該当するデータがない場合は「Null」を返します。Null値をそのままString型などの変数に代入しようとすると、別のエラー(実行時エラー94:Nullの使い方が不正です)が発生するため、以下のように「Nz関数」を組み合わせて安全対策を施す必要があります。
Dim strName As String
' 該当データがなければ空文字("")を返すようにNz関数でガードする
strName = Nz(DLookup("顧客名", "T_顧客マスタ", "顧客ID = 9999"), "")
If strName <> "" Then
MsgBox "顧客名: " & strName
Else
MsgBox "該当顧客は存在しません。"
End If
「取得したいデータが1つだけならDLookup + Nz」、「複数の項目を取得したい場合やループ処理を行うならRecordset + BOF/EOF判定」というように、目的に応じて使い分けるとコードがすっきりします。
注意点3:On Error GoToによるエラーハンドリングとクローズ処理の徹底
どんなに事前に対策を施していても、マルチユーザー環境などでの予期せぬエラー発生の可能性を完全にゼロにすることはできません。万が一エラーが発生した際にも、Accessがフリーズしたり不自然な強制終了を起こしたりしないよう、適切な「エラーハンドラ」を設置しておきましょう。
特に重要なのは、エラーが発生して途中で処理が中断された場合でも、開いたレコードセットやデータベース接続オブジェクトを確実に閉じる(Closeする)ことです。これを怠ると、メモリリークが発生したり、データベースのロックが解除されなくなったりする深刻なトラブルを招きます。
Sub SafeProcedure()
Dim db As DAO.Database
Dim rs As DAO.Recordset
On Error GoTo ErrorHandler ' エラーが発生したらErrorHandlerラベルへジャンプ
Set db = CurrentDb
Set rs = db.OpenRecordset("SELECT * FROM T_売上 WHERE 売上日 = Date()", dbOpenDynaset)
If rs.BOF And rs.EOF Then
MsgBox "本日の売上はまだ登録されていません。", vbInformation
GoTo NormalExit ' 安全に終了処理へ進む
End If
' 通常の処理をここに記述
NormalExit:
' 正常終了時、および安全に処理を抜ける際のクローズ処理
On Error Resume Next ' 後片付け時のエラーは無視する
If Not rs Is Nothing Then
rs.Close
Set rs = Nothing
End If
Set db = Nothing
Exit Sub ' メイン処理を終了
ErrorHandler:
' 予期せぬエラーが発生した場合の通知
MsgBox "予期しないエラーが発生しました。" & vbCrLf & _
"エラー番号: " & Err.Number & vbCrLf & _
"エラー内容: " & Err.Description, vbCritical, "システムエラー"
Resume NormalExit ' 必ず後片付け処理を通す
End Sub
このように、正常系ルートと異常系ルートのどちらを通っても、最終的に「オブジェクトをクローズしてNothingを代入する」コードを通過するように設計するのが、実務におけるプロ仕様のコーディング手法です。
実行時エラー3021を防止するためのチェックリスト
Accessアプリケーションを本番環境にリリースしたり、他のユーザーに配布したりする前に、以下のチェックリストを活用して、プログラムが実行時エラー3021に対して十分に堅牢(エラーが起きにくい状態)であるかを確認してください。
- チェック1:OpenRecordset直後の存在チェック
Recordsetオブジェクトを開いた直後、データを参照したりカーソル移動(Move系)を実行したりする前に、If Not (rs.BOF And rs.EOF)による条件分岐が正しく入っているか? - チェック2:Do Until rs.EOFループ内のMoveNextの有無
レコードセットをループ処理する際、ループ内の最下部(または適切な分岐内)にrs.MoveNextが漏れなく記述されているか?(無限ループやカーソル位置逸脱の防止) - チェック3:動的SQLパラメーターのバリデーション
ユーザー入力などによってSQLのWHERE条件が動的に変化する場合、想定外の入力値(空文字、不正な値、極端な値など)によって抽出結果が0件になる可能性を予期した設計になっているか? - チェック4:削除クエリ実行後のレコード参照
直前の処理で削除クエリ(DELETE文など)を実行してデータを削除した場合、その削除されたレコードに対して、再クエリ(Requery)をかけずに古いレコードセットのままアクセスを試みていないか? - チェック5:徹底したオブジェクトの開放
プログラムが途中でエラー終了、または中途退避(Exit Subなど)する場合でも、エラーハンドラーを経由して必ずrs.CloseおよびSet rs = Nothingが実行されるルーチンになっているか?
これらの項目をプログラム開発時、あるいは不具合のデバッグ時に一つ一つチェックすることで、Accessにおける実行時エラー3021の発生確率は限りなくゼロに近づけることができます。日々の開発業務において、安全なコーディング習慣を身につけていきましょう。
BOFとEOFが両方ともTrueになるのはどのような場合ですか?
テーブルやクエリの抽出結果が「1件も存在しない(レコード数が0件)」の場合に、BOFとEOFが同時にTrueになります。データが完全に空の状態でレコードを参照しようとしたり、カーソルを動かそうとしたりすると、Accessは対象を見失い実行時エラー3021を発生させます。
レコードが0件のときにエラー3021を出さずに、メッセージを表示して処理を安全に終了させるにはどうすればよいですか?
レコードセットを開いた直後に If rs.BOF And rs.EOF Then という条件分岐を入れます。これが成立した(Trueだった)場合には、「データが存在しません」というメッセージを表示(MsgBox)した上で、Exit Sub や Exit Function などを用いて処理を安全に終了(中断)させてください。
ADO(ActiveX Data Objects)を使って外部のSQL Serverなどに接続している場合も同様の対策が必要ですか?
はい、必要です。ADOで使用する Recordset オブジェクトにも BOF プロパティおよび EOF プロパティが備わっています。そのため、DAOと同様に If Not (rs.BOF And rs.EOF) を用いてデータの存在チェックを挟んでから処理を実行してください。
rs.RecordCount を使ってレコード件数をチェックするのはエラー3021対策として有効ですか?
有効です。件数が0件であるかのチェックとして If rs.RecordCount = 0 Then を用いることができます。ただし、DAOではレコードセットを開いた直後は件数が正しくカウントされない仕様があるため、確実性を期すためには If rs.BOF And rs.EOF の判定を使用するか、事前に rs.MoveLast を実行する必要があります。しかし、データがない状態で MoveLast をするとエラー3021が出るため、事前のBOF/EOFチェックの方がより推奨されます。
エラー3021が発生した際、Accessデータベース自体が壊れる原因になりますか?
実行時エラー3021自体は、プログラムの処理ロジック上のエラー(データ未存在によるエラー)であるため、これ自体が原因となってデータベースファイル(accdb/mdb)が破損することはありません。ただし、エラー発生によってVBAが途中で強制終了し、データの書き込みトランザクションが不完全な状態で残ってしまったり、オブジェクトが解放されずにAccessのメモリが不安定になったりする影響はあります。必ずエラーハンドリングを施して、安全に処理を終了させる仕組みにしておくことが重要です。