Access VBAを掌握する極限の知見:DAO.RecordsetのLockEditsが握るマルチユーザー制御の真実
アクセスの開発現場において、単体テストでは完璧に動いていたシステムが、いざ複数名での本番稼働を迎えた途端に「書き込み競合(Write Conflict)」エラーで崩壊する――。この悪夢のような現象の根底にあるのは、多くの開発者が「DAO.RecordsetのLockEditsプロパティ」の挙動をなんとなくで実装しているという事実だ。
ファイルサーバー上で複数のユーザーが同時に`.mdb`や`.accdb`にアクセスする環境において、排他制御の設計ミスはデータ破損や業務停止直結の致命傷となる。今回は、悲観的排他制御と楽観的排他制御のメカニズムを解剖し、現場で即座に使える堅牢なプロダクションコードを授けよう。
—
1. なぜ「なんとなくのRecordset」はマルチユーザー環境で死ぬのか
Access VBAでデータを操作する際、私たちは無意識のうちに `CurrentDb.OpenRecordset` を叩く。しかし、その背後でAccess(Jet/ACEエンジン)がどのようにロックを管理しているかを意識したことはあるだろうか?
マルチユーザー環境の最大の敵は「後勝ち(Last-in wins)」によるデータの隠れた上書きと、「書き込み競合ダイアログ」によるユーザーのフリーズだ。これを制御するのが `Recordset` オブジェクトの `LockEdits` プロパティである。
悲観的排他制御(Pessimistic Locking)
- 該当設定: `LockEdits = True`
- 挙動: レコードを編集モード(`Edit`メソッド)に入れた瞬間から、他のユーザーによるそのレコードへのアクセス(編集・削除)を物理的にロックする。
- メリット: 自分が更新しようとしているデータを他の誰かに横取りされることが絶対にない。
- デメリット: ロック期間が長くなるため、画面を開いたまま離席するユーザーがいると、他の業務が完全にブロックされる。
楽観的排他制御(Optimistic Locking)
- 該当設定: `LockEdits = False`(デフォルト)
- 挙動: 編集開始時点ではロックをかけず、データを保存(`Update`メソッド)する瞬間に、他者がその間にデータを書き換えていないかを検証する。
- メリット: 同時接続性が高く、ロック競合による業務停止が発生しにくい。
- デメリット: 更新競合が発生した場合、「書き込み競合」エラーをトラップし、ユーザーに解決を促す実装(または変更破棄)が必須となる。
—
2. 業務要件に応じた使い分けの鉄則
アーキテクトとして、私はプロジェクトで次のような設計基準をチームに徹底している。
1. 財務データ、在庫数、連番発番など「一瞬の狂いも許されない厳密な数値」の更新
$\rightarrow$ 悲観的排他制御 (`True`) を採用し、確実にロックを確保して処理する。ただし、タイムアウトやトランザクションのスコープを極限まで短くする。
2. マスタメンテナンスや、入力項目の多い汎用的な伝票入力画面
$\rightarrow$ 楽観的排他制御 (`False`) を採用し、システム側で競合を検知して適切にハンドリングする。悲観的ロックで画面を占有させると、現場のオペレーションが確実に破綻する。
—
3. 【プロダクションコード】堅牢な排他制御の実装パターン
ここからは、実務でそのまま使える堅牢なコードを示す。エラーハンドリングとリトライ機構を組み込んだ、プロフェッショナルクオリティのパターンだ。
パターンA:楽観的排他制御による安全なデータ更新(推奨)
エラー3197(他のユーザーがデータを変更した)をキャッチし、適切にユーザーへ通知、または最新データの再読み込みを行う設計にする。
Public Sub UpdateCustomerData_Optimistic(ByVal lngCustomerID As Long, ByVal strNewPhone As String)
Dim db As DAO.Database
Dim rs As DAO.Recordset
Dim strSQL As String
Set db = CurrentDb
strSQL = “SELECT CustomerID, Phone, UpdateCount FROM T_Customers WHERE CustomerID = ” & lngCustomerID
‘ 楽観的ロック(LockEdits = False)でレコードを開く
Set rs = db.OpenRecordset(strSQL, dbOpenDynaset, dbDenyWrite)
If rs.EOF Then
MsgBox “対象のデータが見つかりません。”, vbExclamation
GoTo Cleanup
End If
On Error GoTo ErrorHandler
rs.Edit
rs!Phone = strNewPhone
‘ ※もしUpdateCountのような排他制御用カラムがある場合はここでインクリメント
‘ rs!UpdateCount = Nz(rs!UpdateCount, 0) + 1
rs.Update ‘ ←ここで競合が発生するとエラーになる
MsgBox “データの更新が完了しました。”, vbInformation
Cleanup:
On Error Resume Next
If Not rs Is Nothing Then rs.Close: Set rs = Nothing
Set db = Nothing
Exit Sub
ErrorHandler:
Select Case Err.Number
Case 3197, 3218, 3260 ‘ 書き込み競合やロックエラー
MsgBox “他のユーザーがこのデータを既に変更しています。” & vbCrLf & _
“画面を閉じて再度開き直してください。”, vbCritical, “排他制御エラー”
rs.CancelUpdate
Case Else
MsgBox “予期せぬエラーが発生しました: ” & Err.Description, vbCritical
rs.CancelUpdate
End Select
Resume Cleanup
End Sub
パターンB:悲観的排他制御による確実に守るべき処理
どうしても他のユーザーの割り込みを許したくないクリティカルな処理の場合。ロック取得時のエラー(他者が既にロックしている場合)を確実にトラップする。
Public Function AcquirePessimisticLockAndUpdate(ByVal lngOrderID As Long, ByVal strStatus As String) As Boolean
Dim db As DAO.Database
Dim rs As DAO.Recordset
Dim strSQL As String
AcquirePessimisticLockAndUpdate = False
Set db = CurrentDb
strSQL = “SELECT OrderID, Status, LastModified FROM T_Orders WHERE OrderID = ” & lngOrderID
On Error GoTo LockErrorHandler
‘ 悲観的ロック(LockEdits = True)を指定
Set rs = db.OpenRecordset(strSQL, dbOpenDynaset, dbSeeChanges)
If rs.EOF Then
Set rs = Nothing
Exit Function
End If
‘ Editメソッド実行の瞬間に物理ロックが走る
rs.Edit
rs!Status = strStatus
rs!LastModified = Now
rs.Update
AcquirePessimisticLockAndUpdate = True
Cleanup:
On Error Resume Next
If Not rs Is Nothing Then rs.Close: Set rs = Nothing
Set db = Nothing
Exit Function
LockErrorHandler:
‘ 他のユーザーがすでにレコードをロックしている場合のエラー番号 (3260等)
If Err.Number = 3260 Or Err.Number = 3197 Then
MsgBox “現在、他のユーザーがこのレコードを編集中です。” & vbCrLf & _
“しばらく待ってから再度実行してください。”, vbExclamation, “ロック競合”
Else
MsgBox “エラーが発生しました: ” & Err.Description, vbCritical
End If
If Not rs Is Nothing Then
On Error Resume Next
rs.CancelUpdate
End If
Resume Cleanup
End Function
—
4. チーフアーキテクトからの実践的アドバイス
1. `dbSeeChanges` の付与を忘れるな
SQL ServerなどのODBCデータソースをリンクテーブルとしてDAOで操作する場合、他者が変更したデータを検知するために `dbSeeChanges` オプションを必ず付与すること。これを怠ると、予期せぬ実行時エラーに足元をすくわれる。
2. トランザクションとの組み合わせ
複数テーブルにまたがる更新を行う場合は、`BeginTrans` / `CommitTrans` / `Rollback` を組み合わせるが、悲観的ロックをかけたまま重い外部処理やUI入力を挟むな。トランザクションとロックのスコープは極限まで小さく、短くすることがパフォーマンスとデッドロック回避の絶対条件である。
マルチユーザーでの安定稼働は、運ではなく「設計」で勝ち取るものだ。DAOの挙動を完全に手中に収め、現場から「エラーの出ない強靭なAccessシステム」を構築してほしい。
