【上級】DAO.Recordsetの「LockEdits」プロパティによる、悲観的排他制御と楽観的排他制御の使い分け
開発現場でよく見落とされるが、システムの命運を分ける致命的なポイントがある。それが「同時実行制御(排他制御)」だ。
複数ユーザーが同時にアクセスするAccessデータベースにおいて、レコードの競合をどうハンドリングするか。これを誤ると、データがしれっと上書きされて消える「ロストアップデート(更新損失)」が発生するか、あるいは「常に画面がロックされて使い物にならない」という現場からのクレームの嵐に見舞われることになる。
今回は、DAO.Recordsetが持つ `LockEdits` プロパティを完全に掌握し、業務特性に応じた「悲観的排他制御」と「楽観的排他制御」の最適解をコードベースで伝授する。
—
1. なぜ `CurrentDb.Execute` や安易なSQLだけで業務アプリが破綻するのか
Accessでのデータ操作といえば、`CurrentDb.Execute “UPDATE …”` が手軽で好まれる。しかし、これは「今まさに画面を開いて熟考しているユーザー」の存在を無視して強制書き込みを行うため、実業務の現場では地雷になり得る。
フォームやVBAコード上でレコードを1件ずつ丁寧に処理し、データ整合性を担保しながら安全に更新を行うためには、DAO.Recordsetを駆使した明示的な制御が不可欠だ。その際、レコードをどのタイミングでロックするかを決めるのが `LockEdits` プロパティである。
—
2. LockEditsの二大思想:悲観 vs 楽観
DAOの `Recordset.LockEdits` には、以下の2つの定数を設定できる。
1. `dbOptimistic` (楽観的排他制御:デフォルト)
- 思想: 「人間はそんなに同時に同じデータを書き換えないだろう」という性善説。
- 挙動: レコードを開いている間はロックせず、`Update` メソッドを呼び出したその瞬間にだけ他のユーザーが変更していないかを確認し、ロックをかける。
- メリット: 同時接続数が増えてもパフォーマンスが落ちにくい。
- デメリット: 更新の瞬間に他者に先を越されていた場合、エラー(実行時エラー 3197など)が発生するため、競合時のハンドリングコードが必須となる。
2. `dbPessimistic` (悲観的排他制御)
- 思想: 「人間はいつでもデータを競合させる生き物だ」という性悪説。
- 挙動: `Edit` メソッドを実行した瞬間から、そのレコードを物理的に排他ロックし、他のユーザーの書き込みを完全にブロックする。
- メリット: 更新時のコンフリクト(競合)が絶対に起きないため、金額や在庫数など絶対にミスが許されないクリティカルな処理で無類の強さを発揮する。
- デメリット: ユーザーがフォームを開いたまま席を外したりすると、他のユーザーが一切作業できなくなり、システム全体のボトルネック(デッドロック含む)になる。
—
3. 【実践】プロダクションコード:状況に応じた制御の実装
現場のプロとして、これら二つをどう使い分けるべきか。
基幹システム等で「絶対にデータの整合性を死守すべき重要データの更新」を想定した、実用に耐えうる堅牢なVBAコードを提示する。
以下のコードは、楽観的排他制御 (`dbOptimistic`) を採用しつつ、万が一の競合(他者による同時更新)が発生した際にリトライまたは安全な離脱を行う、プロダクション品質のテンプレートだ。
‘ =========================================================================
‘ プロシージャ名: UpdateInventoryOptimistic
‘ 概要 : 楽観的排他制御を用いた安全な在庫更新処理
‘ 備考 : 複数人が同時にアクセスする環境での標準的な実装パターン
‘ =========================================================================
Public Sub UpdateInventoryOptimistic(ByVal targetProductID As Long, ByVal quantityToAdd As Long)
Dim db As DAO.Database
Dim rs As DAO.Recordset
Dim strSQL As String
Dim retryCount As Integer
Const MAX_RETRIES As Integer = 3
Set db = CurrentDb()
strSQL = “SELECT ProductID, StockQuantity, UpdateCount FROM M_Products WHERE ProductID = ” & targetProductID
‘ 【重要】LockEdits に dbOptimistic を明示的に指定
‘ 開く時点ではロックせず、パフォーマンスを維持する
Set rs = db.OpenRecordset(strSQL, dbOpenDynaset, dbDenyWrite) ‘ ※必要に応じdbDenyWrite等も検討
rs.LockEdits = dbOptimistic
If rs.EOF And rs.BOF Then
MsgBox “対象のレコードが見つかりません。”, vbCritical, “データエラー”
GoTo Cleanup
End If
DataConflictRetry:
On Error GoTo ErrorHandler
rs.Edit
‘ 在庫数を加算
rs!StockQuantity = Nz(rs!StockQuantity, 0) + quantityToAdd
‘ 更新回数(バージョンキー)を持つ場合はここでインクリメントするとより堅牢
If rs.Fields.Item(“UpdateCount”).Properties.Count > 0 Then
rs!UpdateCount = Nz(rs!UpdateCount, 0) + 1
End If
‘ Updateの瞬間に他者との競合チェックが入る
rs.Update
MsgBox “在庫の更新が正常に完了しました。”, vbInformation, “処理成功”
GoTo Cleanup
ErrorHandler:
‘ 3197: 他のユーザーによってデータが変更されています(Access特有の競合エラー)
If Err.Number = 3197 Then
retryCount = retryCount + 1
If retryCount <= MAX_RETRIES Then
' データを最新の状態にリフレッシュして再トライ
rs.Requery
MsgBox "他のユーザーがデータを更新していました。自動リトライします (" & retryCount & "/" & MAX_RETRIES & ")", vbExclamation, "競合検知"
Resume DataConflictRetry
Else
MsgBox "競合が解消されませんでした。処理を中止します。最新データを再確認してください。", vbCritical, "排他制御エラー"
End If
Else
MsgBox "予期せぬエラーが発生しました: " & Err.Description, vbCritical, "システムエラー"
End If
Cleanup:
On Error Resume Next
If Not rs Is Nothing Then
rs.Close
Set rs = Nothing
End If
Set db = Nothing
End Sub
コードの急所解説
- `rs.LockEdits = dbOptimistic` の明示: デフォルトだからと省略せず、意図をもってコード上に明記することが保守性において極めて重要。
- エラー番号 `3197` の捕捉: 楽観的排他制御の肝は、競合エラーが発生したときのキャッチとリカバリ。`rs.Requery` で最新データを引き直して再試行するロジックを入れることで、実用性が飛躍的に向上する。
—
4. 悲観的排他制御を使うべき唯一無二のシチュエーション
では、`dbPessimistic` はいつ使うべきか?
それは、「絶対に後勝ち・先勝ちの不整合を起こしてはならない、かつ処理時間が一瞬で終わる排他処理」だ。例えば、連番の払い出しマスタや、厳密な引き当て処理など、他者に割り込まれる余地を1ミリも与えたくないバッチ処理に近いトランザクションの核で使用する。
ただし、悲観的ロックをフォームの画面上に直接バインドして長時間保持させると、「画面を開いたままトイレに行くユーザー」によってシステム全体がデッドロック状態に陥るという大惨事を引き起こす。
そのため、悲観的ロックは「VBAのプロシージャ内でレコードセットを開き、極めて短時間(数ミリ秒〜数秒以内)で処理を完結させて即座にクローズする」スコープでのみ使用すべきである。
—
5. チーフアーキテクトからの提言
Accessバックエンドがファイルサーバー上の `.accdb` である場合、ネットワークの瞬断やファイルロックの特性上、悲観的排他制御はリスクが大きい。
- 基本方針: 標準は常に `dbOptimistic`(楽観的排他制御) を採用し、競合時のリトライ・メッセージハンドリングを実装する。
- 例外方針: 局所的なトランザクションの整合性がどうしても必要な場合のみ、短命なスコープで `dbPessimistic` を適用する。
この使い分けをロジカルに設計に組み込めるかどうかが、プロのAccessエンジニアと、場当たり的なコードを書く素人を見分ける決定的な境界線となる。
現場の信頼を勝ち取る堅牢なアーキテクチャを、あなたの手で実装してほしい。
