【テクニカル・上級編】【中級】DAO.Recordsetの「LockEdits」プロパティによる、悲観的排他制御と楽観的排他制御の使い分け – Access VBA解析バイブル

スポンサーリンク

Access VBAを掌握する極限の知見:DAO.Recordset「LockEdits」が支配するマルチユーザー排他制御の深淵

マルチユーザー環境のAccessバックエンドにおいて、データ競合(Lost Update:更新損失)はエンジニアリングの死活問題である。
ネットワークの向こう側で、別セッションのユーザーがコンマ数秒差で同一レコードを書き換えたとき、あなたの書いたVBAコードは沈黙するか、意図せぬサイレント上書きを引き起こす。

この致命的な競合を防ぐための防壁が、DAO(Data Access Objects)の `LockEdits` プロパティである。

今回は、悲観的排他制御と楽観的排他制御のメカニズムを解剖し、現場の極限環境で耐えうる堅牢な実装パターンを提示する。レガシーなAccessの限界を見据え、オブジェクトのライフサイクル管理とエラーハンドリングの極致を共有しよう。

1. 悲観的排他制御(`dbOptimistic` vs `dbPessimistic`)の残酷な真実

DAOの `OpenRecordset` メソッドや `Recordset.LockEdits` プロパティには、主に2つの定数が存在する。

  • `dbPessimistic`(悲観的排他制御): レコードの「編集開始(`.Edit`)」から「確定(`.Update`)」まで、データベースエンジン(JET/ACE)レベルで対象ページ/レコードをロックする。
  • `dbOptimistic`(楽観的排他制御): 編集開始時はロックせず、`.Update` を実行してディスクに書き込む「その瞬間」に初めて他のトランザクションとの競合を確認する。

なぜ悲観的ロックはAccess(Jet/ACE)において劇薬なのか?

一見すると、悲観的排他制御は安全に見える。「編集しようとした瞬間に他者の介入をブロックする」のだから。しかし、これがデスクトップデータベースのファイル共有モデル(SMBプロトコル経由の`.accdb`ファイルアクセス)の上で動くとき、悲劇が起きる。

1. ネットワーク切断時のハングアップ: 悲観的ロック中にクライアントのLANケーブルが抜けた場合、Accessのロックファイル(`.laccdb`)にゾンビロックが残り、他の全ユーザーが締め出される。
2. 長寿命トランザクションの罪: ユーザーがフォームを開いたまま離席しただけで、バックエンドのレコードが数分間ロックされ続ける。

シニアエンジニアの鉄則:
WebアプリケーションのORM(Entity FrameworkやHibernateなど)の常識を持ち込んではならない。ファイル共有型DBであるAccessにおいて、`dbPessimistic` の安易な多用はシステム全体をデッドロックの淵に追い込む。基本戦略は常に「楽観的排他制御(`dbOptimistic`)」であり、悲観的ロックは「絶対に競合が許されない数秒の金融・在庫処理」でのみ、厳格なタイムアウト制御と共に局所的に使うべきである。

2. 実装パターン:楽観的排他制御における「3022/3186/3197」エラーの捕縛

楽観的排他制御(`dbOptimistic`)を採用した場合、 `.Update` メソッドの実行時に他者による改変が検知されると、DAOは容赦なくランタイムエラーを発生させる。

ここで発生する代表的なトラップエラー:

  • エラー 3197: 「別のユーザーがデータを変更しました。処理を続行しますか?」
  • エラー 3186/3218: 「ファイルがロックされています。」

これらを適切にハンドリングし、ユーザーに「再試行」または「破棄」の選択権を与えるプロシージャの実装例を示す。

実用VBAコード:堅牢な楽観的排他制御のトランザクション実装

Option Compare Database
Option Explicit

‘ =========================================================================
‘ プロジェクト名: 楽観的排他制御による安全なレコード更新サンプル
‘ 概要: 複数ユーザー環境下でのデータ競合を検知し、安全にハンドリングする
‘ =========================================================================
Public Sub UpdateProductPrice(ByVal lngProductID As Long, ByVal curNewPrice As Currency)
Dim db As DAO.Database
Dim rs As DAO.Recordset
Dim strSQL As String
Dim lngRetryCount As Long
Const MAX_RETRIES As Integer = 3

‘ CurrentDbは毎回インスタンスが生成されるため、必ず変数に格納して参照・解放する
Set db = CurrentDb()

strSQL = “SELECT ProductID, UnitPrice, UpdateCount FROM T_Products WHERE ProductID = ” & lngProductID

‘ 【極限知見】LockEdits に dbOptimistic を明示的に指定
‘ ※デフォルトもdbOptimisticだが、コードの意図を明確にするために必ず記述する
Set rs = db.OpenRecordset(strSQL, dbOpenDynaset, dbDenyRead, dbOptimistic)

If rs.EOF Then
MsgBox “対象のレコードが見つかりません。”, vbCritical, “排他制御エラー”
GoTo Cleanup
End If

RetryTransaction:
On Error GoTo ErrorHandler

rs.Edit
‘ データを書き換える
rs!UnitPrice = curNewPrice

‘ 排他制御のカウンター(任意:オプショナルなバージョン管理列)がある場合のインクリメント
If rs.Fields.Item(“UpdateCount”).OriginalValue = rs!UpdateCount Then
rs!UpdateCount = Nz(rs!UpdateCount, 0) + 1
End If

‘ ここで初めて競合チェックが行われる(楽観的ロックの極み)
rs.Update

MsgBox “更新が正常に完了しました。”, vbInformation, “成功”
GoTo Cleanup

ErrorHandler:
Select Case Err.Number
Case 3197, 3218, 3260 ‘ データ競合またはロック競合のエラー番号群
lngRetryCount = lngRetryCount + 1
If lngRetryCount <= MAX_RETRIES Then ' 競合発生時はレコードを再同期(Requery)してリトライ rs.Requery ' 少しウェイトを入れる場合はここで DoEvents や API Sleep を挟む LogDebug "排他競合を検知。リトライします (" & lngRetryCount & "/" & MAX_RETRIES & ")" Resume RetryTransaction Else MsgBox "他のユーザーによってデータが頻繁に更新されています。" & vbCrLf & _ "時間を置いて再度実行してください。", vbCritical, "競合タイムアウト" End If Case Else ' 予期せぬエラー MsgBox "予期せぬエラーが発生しました: [" & Err.Number & "] " & Err.Description, vbCritical End Select Cleanup: ' 【極限知見】オブジェクトのライフサイクル管理:開いた順とは逆の順序で確実に解放 If Not rs Is Nothing Then rs.Close Set rs = Nothing End If If Not db Is Nothing Then Set db = Nothing End If Exit Sub LogDebug: ' デバッグ用ログ出力(必要に応じてFSO等でファイル出力に拡張可能) Debug.Print "[" & Now & "] " & lngProductID End Sub ---

3. メモリ最適化とオブジェクトリークの完全根絶

Access VBA開発において、もっとも恐ろしいのは 「見えないメモリリーク」 である。
特に `CurrentDb()` は呼び出すたびに新しい DAO.Database オブジェクトをメモリ上のヒープ領域に生成する。これをローカル変数に受け受け取らずに `CurrentDb.OpenRecordset…` のようにチェーンメソッド的に記述するコードは、ガベージコレクションのタイミングが不安定なVBA環境下において、確実にメモリ断片化(リソース枯渇)を引き起こす。

オブジェクト解放の鉄則

1. `CurrentDb` は必ず変数に代入して使い回す
2. `Recordset` は処理が終わったら即座に `.Close` し、`Set rs = Nothing` で参照を断つ
3. エラーハンドラー(`GoTo Cleanup`)を必ず経由させ、異常終了時であってもメモリリークを防ぐ構造にする

4. チーフアーキテクトからの提言

マルチユーザー環境でのAccessアプリケーション構築は、現代のC/SシステムやWebシステムに比べ、インフラストラクチャの許容量がシビアである。ネットワークのパケットロス、ファイルサーバーのIOPSの限界、そしてJet/ACEエンジンのファイルロック機構の特性。

これらを無視して「動けばいい」で作られたVBAコードは、データ破損(`.ldb` / `.laccdb` の肥大化や破損)という最悪の結末を招く。

`LockEdits` の特性を深く理解し、`dbOptimistic` を軸としたエラーハンドリングと、厳格なオブジェクトライフサイクル管理をコードに定着させること。それこそが、レガシー環境の寿命を延ばし、真に信頼性の高いシステムを担保唯一の道である。

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