【実務・中級編】DAO.Recordsetの「AddNew」と「Update」の間に潜む排他制御の罠と回避策 – Access VBA解析バイブル

スポンサーリンク

Access VBAを掌握する極限の知見:DAO.Recordsetの「AddNew」と「Update」に潜む排他制御の罠と回避策

こんにちは。チーフアーキテクトの私だ。
開発現場を見渡すと、Access VBAの入門書に書いてある通りのコードをそのままマルチユーザー環境に投入し、原因不明の「書き込み競合(Write Conflict)」エラーに頭を抱えているエンジニアがあまりにも多い。

「なぜ、ローカルテストでは完璧に動くのに、実運用(共有サーバー)になると突然止まるのか?」
「エラーハンドリングを入れたはずなのに、ユーザーが勝手にレコードを破壊してしまうのはなぜか?」

今回は、DAOの `Recordset` における `AddNew` と `Update` の間に存在する排他制御の暗部と、プロの現場で通用する堅牢なリトライ設計のすべてを伝授する。

1. なぜ「AddNew」と「Update」の間は戦場なのか?

Access(Jet / ACE データベースエンジン)のオプティミスティック(楽観的)排他制御の仕組みを理解しているだろうか?

`AddNew` を実行した瞬間、VBAのメモリ空間上に新規レコードのバッファが生成される。しかし、この時点ではデータベースには何も書き込まれていない。
実際に物理的なレコードとして書き込まれるのは、`Update` メソッドが実行された「その瞬間」だ。

[VBA: AddNew] ──> (メモリ上にバッファ生成) ──> [フィールド代入] ──> [VBA: Update] ──> 【ここで書き込み&競合判定】

この「バッファ生成から `Update` に至るまでのわずかな時間差」が、マルチユーザー環境における最大の罠となる。
特に、自動採番(オートナンバー)フィールドを持つテーブルにおいて、複数ユーザーがほぼ同時に `AddNew` を叩いた場合、インデックスやページのロック競合、あるいはタイムスタンプの不整合により、容赦なくランタイムエラー(書き込み競合)が発生するのだ。

ここで生半可な `On Error Resume Next` などを書く者は、エンジニア失格と言わざるを得ない。エラーを握りつぶした結果、「データが登録されたつもりが、実は宙に浮いて消えていた」という最悪のデータ欠損を引き起こすからだ。

2. 堅牢なリトライ設計:プロダクションコードの全貌

プロのアーキテクトが書くべきコードとは、「失敗を前提とし、失敗からスマートに回復するコード」だ。
書き込み競合(エラー番号: 3186, 3197, 3260など)が発生した際、即座に諦めるのではなく、「数ミリ秒のウェイトを挟んでリトライする」仕組みを実装する。

以下に、実務でそのまま使える、極限まで洗練されたレコード追加関数を提示する。コピペして君のプロジェクトの標準モジュールに組み込んでほしい。

Option Compare Database
Option Explicit

‘ =========================================================================
‘ 模範的実装:排他制御リトライ機構付きレコード追加関数
‘ =========================================================================
Public Function SafeAppendRecord(ByVal strTableName As String, ByVal dictFieldValues As Object) As Boolean
Dim db As DAO.Database
Dim rs As DAO.Recordset
Dim maxRetries As Integer
Dim retryCount As Integer
Dim waitTime As Double
Dim i As Variant

‘ — 設定値 —
maxRetries = 5 ‘ 最大リトライ回数
waitTime = 0.5 ‘ 初期待機時間(秒)

Set db = CurrentDb()

‘ トランザクションはレコード単位ではなく、競合リトライの外側か内側かを慎重に設計する
‘ ここではDAOのテーブルロック競合を個別解決するため、Recordsetオープン単位で制御

retryCount = 0

Retry_Process:
On Error GoTo Error_Handler

‘ 稼働中のパフォーマンスを考慮し、ダイナセットまたはスナップショットを適切に選択
‘ 新規追加のみであれば dbOpenDynaset が安全
Set rs = db.OpenRecordset(strTableName, dbOpenDynaset, dbDenyRead) ‘ ※必要に応じてオプション調整

rs.AddNew

‘ Dictionary等で渡されたフィールド値をごっそり流し込む
For Each i In dictFieldValues.Keys
rs.Fields(i).Value = dictFieldValues(i)
Next i

‘ 【運命の瞬間】ここで書き込み競合が発生しうる
rs.Update

‘ 成功
rs.Close
Set rs = Nothing
Set db = Nothing
SafeAppendRecord = True
Exit Function

Error_Handler:
‘ DAOのエラー番号を精査
‘ 3186: 別のユーザーがデータをロックしています
‘ 3197: 別のユーザーがデータを変更しました
‘ 3260: テーブルがロックされています
Select Case Err.Number
Case 3186, 3197, 3260, 3033, 3027
retryCount = retryCount + 1
If retryCount <= maxRetries Then ' オブジェクトを確実に解放してリトライに備える If Not rs Is Nothing Then On Error Resume Next rs.Close Set rs = Nothing On Error GoTo Error_Handler End If ' バックオフ戦略:少しずつ待機時間を延ばしながらリトライ Call SleepByAPI(waitTime 1000) waitTime = waitTime 1.5 ' 待機時間を指数関数的に増加 Resume Retry_Process Else MsgBox "排他制御の競合が解決しませんでした。時間を置いて再度実行してください。" & vbCrLf & _ "詳細: " & Err.Description, vbCritical, "システムエラー" GoTo Safe_Exit End If Case Else ' 想定外のエラーはそのままスロー MsgBox "予期せぬエラーが発生しました [" & Err.Number & "]: " & Err.Description, vbCritical GoTo Safe_Exit End Select Safe_Exit: If Not rs Is Nothing Then On Error Resume Next rs.Close Set rs = Nothing End If Set db = Nothing SafeAppendRecord = False End Function ' 精度高いウェイトを実現するためのAPI宣言(32bit/64bit両対応) If VBA7 Then Private Declare PtrSafe Sub Sleep Lib "kernel32" (ByVal dwMilliseconds As Long) Else Private Declare Sub Sleep Lib "kernel32" (ByVal dwMilliseconds As Long) End If Private Sub SleepByAPI(ByVal lngMilliSeconds As Long) Sleep lngMilliSeconds End Sub ---

3. コードのアーキテクチャ的解説:なぜこの設計なのか?

① 指数バックオフ(Exponential Backoff)の採用

競合が発生した際、ミリ秒単位ですぐにリトライを繰り返すと、サーバーやネットワークの負荷(ファイルロックの奪い合い)がさらに悪化する。
上記のコードでは、リトライするごとに待機時間を `1.5倍` に増やしている。これにより、他のユーザーの処理が完了するのをスマートに待つことができる。

② エラー発生時の確実なオブジェクト破棄

競合エラー(3186など)をキャッチした際、`Recordset` が中途半端な状態でメモリに残っていると、次のループでリソースリークやさらなるロック競合を引き起こす。
エラーハンドラ内で必ず `rs.Close` と `Set rs = Nothing` を挟み、クリーンな状態で `Resume Retry_Process` を踏むのが鉄則だ。

③ インターフェースの抽象化(Dictionaryの活用)

ハードコーディングされた `rs!Field1 = “A”` のような書き方は保守性を下げる。引数に `Scripting.Dictionary` などのコレクションを受け取る設計にすることで、どんなテーブルに対してもこのリトライ機構を「使い回す」ことが可能になる。これがプロのコードだ。

4. 現場のエンジニアへ:ファイルサーバー運用における最終警告

最後に、Accessをマルチユーザーで運用する際の「絶対の鉄則」を釘を刺しておこう。

1. フロントエンドとバックエンドの完全分離
UI(フォーム・VBA)を持つフロントエンドは必ず各クライアントPCのローカルに配置し、テーブルのみを置いたバックエンド(`.accdb`)をファイルサーバーに置くこと。これを怠ると、今回のリトライ処理を入れてもネットワークトラフィックの限界でシステムは崩壊する。
2. 長時間のトランザクションを避ける
`AddNew` から `Update` の間、あるいはトランザクション(`BeginTrans` ~ `CommitTrans`)の内部で、ユーザーへの `MsgBox` を挟んだり、重い外部処理を行ってはならない。ロック保持時間が長引けば長引くほど、システム全体のスループットが低下する。

排他制御を制する者は、Access開発を制する。
甘い設計で構築されたシステムは、ユーザーが増えた瞬間に音を上げる。ぜひこの知見を君のプロジェクトに組み込み、ビクともしない堅牢なシステムを作り上げてほしい。

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