DAOのDeleteメソッドで発生する「削除不可」エラーを完全掌握せよ:現場の絶望を打破する極限のデバッグと実装
長年、Access VBAによる基幹システムや現場のオペレーションツールを保守してきたエンジニアであれば、一度は遭遇する悪夢がある。
それが、`Recordset.Delete` メソッド実行時に突如として発生する「削除できませんでした」というエラー、あるいはその背後で沈黙する予期せぬ挙動だ。
ADO全盛の時代にあっても、Accessのローカルエンジン(ACE/Jet)を直叩きするDAO(Data Access Objects)は、その圧倒的な速度と親和性から手放せない。しかし、DAOの抽象化されたエラーハンドリングの裏側では、Jet/ACEデータベースエンジン、OSのファイルロック、そしてウィンドウズメッセージの複雑な糸が絡み合っている。
今回は、シニアアーキテクトの視点から、DAOの削除エラーの根本原因を解体し、現場で即座に使える極限の対策コードを提示する。
—
1. なぜDAOのDeleteは失敗するのか?(アーキテクチャの深層)
DAOでレコードを削除できない原因は、大別して以下の3つに集約される。これらはエラー番号だけで判別することが難しく、エンジン内部のステートを理解している者だけが看破できる領域だ。
1. 参照整合性(Cascading/Restricted Integrity)の違反
リレーションシップで「連鎖削除」が有効になっていない子レコードが存在する場合、親の削除は容赦なく拒絶される。厄介なのは、このエラーがDAO層ではなく、Jetエンジン層でトラップされ、開発者が意図しない汎用エラーとして返される点だ。
2. 多重ロックとトランザクションの膠番(デッドロック)
フォームのレコードソースとDAOのレコードセットが競合している場合や、他のユーザ(あるいは同一プロセスの別インスタンス)がページロックを保持している場合、Deleteはタイムアウトを引き起こす。
3. カレントレコードポインタの幽霊化
削除処理の直前で他のプロセスによってレコードが消去されている、あるいはブックマークが無効化されている状態での削除試行。
—
2. 現場で使える「真のデバッグ手法」
安易な `On Error Resume Next` でエラーを握りつぶすコードは、技術的負債の温床でしかない。どの制約に抵触したのかをプログラム側で正確に特定し、ログまたはUIへ適切にフィードバックする必要がある。
以下のコードは、DAOの `Errors` コレクションを完全に走査し、Jetエンジンのネイティブエラーコードを捕捉する堅牢な削除ルーチンの骨子である。
Public Sub SafeDeleteRecord(ByVal rst As DAO.Recordset)
Dim errLoop As DAO.Error
Dim lngJetError As Long
On Error GoTo ErrorHandler
‘ トランザクションの開始(必要に応じてDBオブジェクトを指定)
rst.ActiveConnection.BeginTrans
‘ 削除実行
rst.Delete
‘ コミット
rst.ActiveConnection.CommitTrans
Exit Sub
ErrorHandler:
‘ データベースエンジンのエラー情報を逆転写して解析
If Not rst.ActiveConnection Is Nothing Then
rst.ActiveConnection.Rollback
End If
lngJetError = 0
If DAO.Errors.Count > 0 Then
For Each errLoop In DAO.Errors
‘ 参照整合性エラーのトラップ (代表例: 3200 = レコードはロックされています, 3092 = 関連レコードが存在するため…)
‘ Jetエラーコードに応じた分岐処理
lngJetError = errLoop.Number
Debug.Print “DAO Error: ” & errLoop.Number & ” – ” & errLoop.Description
Next errLoop
End If
‘ 独自のエラーハンドリングへ波及
Select Case lngJetError
Case 3200
MsgBox “指定されたレコードは現在他のユーザーによってロックされています。”, vbCritical, “排他制御エラー”
Case 3092, 3216 ‘ ※環境に応じた制約エラー番号
MsgBox “このレコードを参照している子データが存在するため、削除できません。” & vbCrLf & _
“関連するデータを先に整理してください。”, vbExclamation, “参照整合性エラー”
Case Else
MsgBox “予期せぬエラーにより削除に失敗しました。(Error: ” & lngJetError & “)”, vbCritical, “システムエラー”
End Select
End Sub
—
3. メモリ最適化とオブジェクトライフサイクルの極意
Access VBAのメモリ管理は、CLR(Common Language Runtime)のようなガベージコレクションに依存できない。特にDAOの `Recordset` や `Database` オブジェクトをループ内で生成・破棄する場合、明示的な解放を行わないと、メモリリークおよびJetエンジンのLDBファイル(ロックファイル)の肥大化・ゴースト化を招く。
以下の鉄則を遵守せよ。
- ローカル変数のRecordsetは、処理の最後に必ず `Close` し、変数に `Nothing` を代入する。
- フォームのRecordsetCloneを操作する場合、クローン自体のポインタ管理に細心の注意を払う。
Public Sub ProcessBatchDeletion()
Dim wrk As DAO.Workspace
Dim db As DAO.Database
Dim rst As DAO.Recordset
Set wrk = DBEngine.Workspaces(0)
Set db = wrk.Databases(0)
‘ 厳密なパラメータを指定してレコードセットを開く
Set rst = db.OpenRecordset(“T_TargetData”, dbOpenDynaset, dbDenyWrite, dbOptimistic)
Do Until rst.EOF
If rst!Status = 99 Then
‘ トランザクションを内包した安全な削除
rst.Delete
End If
rst.MoveNext
Loop
CleanUp:
‘ 【極めて重要】逆順でのオブジェクト解放
If Not rst Is Nothing Then
rst.Close
Set rst = Nothing
End If
If Not db Is Nothing Then
db.Close
Set db = Nothing
End If
Set wrk = Nothing
Exit Sub
ProcError:
MsgBox “バッチ処理中に致命的なエラーが発生しました: ” & Err.Description, vbCritical
Resume CleanUp
End Sub
—
4. ウィンドウズAPIを活用した高度な排他制御の検知
大規模なマルチユーザー環境において、Accessのデフォルトのロック機構だけでは制御しきれない瞬間がある。どうしても排他制御の確実性を高めたい場合、Windows API(例: `LockFileEx` やプロセス間同期)を持ち出すまでもなく、DAO自体の排他モード(`dbDenyRead` / `dbDenyWrite`)を適切に設計の段階で組み込むべきだ。
しかし、別プロセスがファイルレベルでAccessの `.accdb` を握っている場合、VBAからは手が出せない。そのような時は、エラー3200(レコードロック)を検知した瞬間に、数ミリ秒のウェイトを挟んでリトライする「指数バックオフアルゴリズム」をVBAで実装するのがプロフェッショナルのアプローチである。
‘ リトライロジックを組み込んだ削除ラッパー
Public Function SafeDeleteWithRetry(ByVal rst As DAO.Recordset, Optional ByVal MaxRetries As Integer = 3) As Boolean
Dim lngAttempt As Integer
Dim lngWaitTime As Long
lngAttempt = 0
lngWaitTime = 500 ‘ 初期待機時間 500ms
RetryLoop:
lngAttempt = lngAttempt + 1
On Error GoTo HandleError
rst.Delete
SafeDeleteWithRetry = True
Exit Function
HandleError:
‘ 3200: 락エラーの場合のみリトライを許可
If Err.Number = 3200 And lngAttempt <= MaxRetries Then
On Error GoTo 0
' Windows APIのSleepを用いた精密なウェイト (ミリ秒単位)
Call Sleep(lngWaitTime)
lngWaitTime = lngWaitTime 2 ' バックオフ
Resume RetryLoop
Else
' その他のエラーは上位へスロー、または独自処理
SafeDeleteWithRetry = False
MsgBox "削除処理が競合しました。時間を置いて再度実行してください。", vbCritical
End Function
End Function
(※注: `Sleep` 関数を使用するためには、標準モジュールの宣言部に `Declare PtrSafe Sub Sleep Lib "kernel32" (ByVal dwMilliseconds As LongPtr)` の記述が必要となる)
---
総括
DAOの `Delete` メソッドにおける「削除不可」エラーは、単なるバグではない。それはデータベースが発する「整合性と安全性を守りたがっているシグナル」である。
エラーを隠蔽するのではなく、Jetエンジンのエラーコレクションを紐解き、オブジェクトのライフサイクルを慈しみ、マルチユーザー環境を見据えた排他制御をコードに落とし込むこと。この泥臭くも洗練されたエンジニアリングの積み重ねこそが、レガシーシステムを「現役の要塞」たらしめる唯一の道なのだ。
