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