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

スポンサーリンク

DAO.Recordsetの「AddNew」と「Update」の間に潜む排他制御の罠と回避策

マルチユーザー環境のAccessバックエンド(分割構成のACE/Jetデータベースエンジン)において、レコードの追加処理は一見すると枯れた技術のように思える。しかし、`AddNew` から `Update` に至るまでのマイクロ秒単位のタイムラインには、開発者が容易に踏み外す「排他制御の地雷」が埋まっている。

今回は、この不可避の競合状態に対して、DAOの挙動、メモリ最適化、そして実戦で生き残るための高度なリトライ機構の設計思想を徹底的に解剖する。

1. 悲劇のメカニズム:なぜ「AddNew」と「Update」の間で衝突するのか

多くのプログラマは、`CurrentDb.OpenRecordset` で得たオブジェクトに対し、以下のようなコードを書く。

Dim rs As DAO.Recordset
Set rs = CurrentDb.OpenRecordset(“T_Stock”, dbOpenDynaset)
rs.AddNew
rs!ItemCode = “A-001”
rs!Quantity = 100
rs!Update ‘ <-- ここで暗黙の排他制御が発動する rs.Close Set rs = Nothing このコードの何が問題か。 Jet/ACEエンジンは、`AddNew` から `Update` が呼び出されるまでの間、ページロックまたはレコードロックを厳密には確定させていない。正確には、`Update` メソッドが実行された瞬間、バッファ上の変更内容を物理ページに書き込もうと試みる。

ここでマルチユーザー環境特有の罠が発動する。
1. ユーザーAとユーザーBが、ほぼ同時に同じテーブルに対して `AddNew` を実行する。
2. 自動採番(Autonumber / 応答なし等)のインデックスページや、最後の物理データページに対する競合が発生する。
3. Accessはこれを「書き込み競合(Write Conflict)」として検知し、ランタイムエラー(通常は実行時エラー 3186 または 3260「他のユーザーが…」)を発生させる。

特に、自動採番(長整数型/GUID)のインデックス競合は、同時実行性が高いシステムにおいて致命的なボトルネックとなる。

2. メモリ最適化とオブジェクトのライフサイクル管理

排他制御を語る前に、Access VBAにおける致命的なメモリリークと、それによる「不要なロックの保持」について言及しなければならない。

素人が書いたコードによく見られる `CurrentDb.OpenRecordset` の連続使用は、内部で隱れセッションやクエリキャッシュを肥大化させ、Jetエンジンのロックマネージャーに余計な負荷をかける。

チーフアーキテクトの鉄則:オブジェクトのスコープと即時解放

DAOオブジェクト(`Recordset`, `Database`)は、ガベージコレクションの慈悲を待ってはならない。用が済んだ瞬間に `Close` し、メモリ空間から抹消すべきである。

‘ 良い例:トランザクションと明示的破棄の徹底
Public Sub SafeInsertRecord()
Dim wrk As DAO.Workspace
Dim db As DAO.Database
Dim rs As DAO.Recordset

Set wrk = DBEngine.Workspaces(0)
Set db = wrk.Databases(0)

On Error GoTo ErrorHandler

‘ トランザクション開始により、ダーティリードを防ぎ、ロック範囲を最小化
wrk.BeginTrans

Set rs = db.OpenRecordset(“T_OrderHeader”, dbOpenDynaset, dbAppendOnly)
rs.AddNew
rs!OrderDate = Now
rs!CustomerCode = “C9999”
rs!Update

wrk.CommitTrans
GoTo Cleanup

ErrorHandler:
wrk.Rollback
MsgBox “エラー発生: ” & Err.Number & ” – ” & Err.Description, vbCritical

Cleanup:
‘ 逆順での厳密な解放
If Not rs Is Nothing Then
rs.Close
Set rs = Nothing
End If
Set db = Nothing
Set wrk = Nothing
End Sub

`dbAppendOnly` オプションの指定に注目してほしい。新規追加しか行わないことが確実な場合、このオプションを付与することで、Jetエンジンは既存レコードの読み込みキャッシュを行わず、ネットワークトラフィックとメモリ消費を極限まで抑制する。これがパフォーマンスチューニングの第一歩だ。

3. 排他制御の回避策:リトライ機構(指数バックオフ)の実装

マルチユーザー環境における競合は「防ぐ」ことが不可能であるため、「発生した後にスマートにリトライする」アーキテクチャを構築する以外に道はない。

ここで、Windows APIの `Sleep` 関数を組み合わせた、実践的なリトライアルゴリズムを提示する。単にループを回すだけの愚直なリトライは、CPUを不必要にスパイクさせ、競合状態を悪化させるだけである。

極限の頑健性を持つインサート・ラッパー関数

以下のコードは、書き込み競合(エラー 3186, 3260, 3188 など)を検知した場合に、ミリ秒単位で待機時間を延ばしながら(指数バックオフ)リトライを行うプロフェッショナル向けの実装である。

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

‘ DAO書き込み競合のエラー番号定数
Const ERR_WRITE_CONFLICT As Long = 3186
Const ERR_LOCKED_RECORSET As Long = 3260
Const ERR_CANNOT_UPDATE As Long = 3027

Public Function ExecuteWithRetry(ByVal sql As String, Optional ByVal maxRetries As Long = 5) As Boolean
Dim db As DAO.Database
Dim attempt As Long
Dim delay As Long

Set db = CurrentDb()
attempt = 0
delay = 200 ‘ 初期遅延 200ms

On Error GoTo RetryHandler

DoRetry:
attempt = attempt + 1

‘ SQL実行による追加(またはRecordset操作)
db.Execute sql, dbFailOnError

ExecuteWithRetry = True
Exit Function

RetryHandler:
If Err.Number = ERR_WRITE_CONFLICT Or Err.Number = ERR_LOCKED_RECORSET Then
If attempt <= maxRetries Then ' 競合の場合は少し待ってリトライ(指数バックオフ + 乱数によるジッター) Sleep delay + (Rnd 100) delay = delay 2 ' 待機時間を倍増 Resume DoRetry End If End If ' 致命的なエラー、またはリトライ上限オーバー MsgBox "データベースの書き込み競合が解消されませんでした。" & vbCrLf & _ "エラー: " & Err.Number & " - " & Err.Description, vbCritical, "システムエラー" ExecuteWithRetry = False End Function ---

4. アーキテクチャの極み:ADODB / SQL Server 移行への布石

Access VBAの限界を悟ったシニアエンジニアであれば、ローカルのJetエンジンに固執せず、バックエンドを SQL Server (Azure SQL Database 等) にアップサイジングする選択肢を常に視野に入れているはずだ。

DAOからADODB(または直接ODBC経由のパススルー)へ移行する場合、排他制御の概念は「楽観的同時実行制御(Optimistic Concurrency)」や「行ロック(Row-level locking)」へとシフトする。

しかし、Access前端(Frontend)からDAOを使い続ける必要があるレガシー環境においては、以下の設計指針を絶対に死守すべきである。

1. 自動採番の罠を避ける: 大量同時書き込みが発生するテーブルでは、可能な限りクライアント側で一意なID(GUIDやUUID、あるいは独自の採番テーブルによる制御)を生成し、Jetエンジンの自動採番インデックスの競合をバイパスする。
2. トランザクションのスコープを最小化する: `BeginTrans` から `CommitTrans` までの間隔は、ネットワークパケットの往復やUIの介入を排除し、数マイクロ秒で完結させる。

結言

Access VBAは「おもちゃの言語」ではない。その背後にあるDAOとJet/ACEエンジンの挙動を完全に支配下においた時、それは堅牢なミッションクリティカル・システムをも支える強力なインターフェースへと変貌する。

`AddNew` と `Update` の間に潜む魔物は、適切なメモリ管理と、洗練されたリトライアルゴリズムによってのみ鎮静化できる。コードの行数ではなく、背後にあるリソースのライフサイクルと排他制御のタイムラインを視覚化できるか否か──それこそが、真のプロフェッショナルとアマチュアを分かつ境界線である。

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