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

スポンサーリンク

【上級】DAO.Recordsetの「LockEdits」プロパティによる、悲観的排他制御と楽観的排他制御の使い分け

数多くのレガシーシステムを解体・再構築してきた中で、Access VBAが持つ最大のポテンシャル、そして最大の罠は、常に「データアクセスの抽象化の裏側」に潜んでいた。

多くのプログラマは、`CurrentDb.OpenRecordset` を呼び出し、思考停止でレコードを更新する。しかし、多重度の高いネットワーク環境、あるいはRDBとAccessのハイブリッド環境において、排他制御(Concurrency Control)のメカニズムを誤れば、システムは瞬く間にデッドロックの渦に飲み込まれるか、あるいは「知らぬ間に他人のデータを上書きする」という致命的なデータ破壊を引き起こす。

今回は、DAO(Data Access Objects)の心臓部である `Recordset.LockEdits` プロパティに焦点を当て、悲観的排他制御(Pessimistic Locking)楽観的排他制御(Optimistic Locking) の本質的な挙動、そして実務の現場で生き残るための最適解を提示する。

1. 排他制御のパラダイム:なぜ `LockEdits` なのか

Access/Jet(ACE)エンジンにおけるレコードロックは、SQL Serverなどのクライアント/サーバーRDBとは異なり、ファイル共有の特性を引き継いでいる。ここで鍵を握るのが `DAO.Recordset` オブジェクトの `LockEdits` プロパティだ。

  • `dbEditLocked` (True) : 悲観的排他制御
  • レコードを「編集モード(`Edit` メソッド)」に入れた瞬間から、他のトランザクションによる当該レコードの書き込みを物理的・論理的にブロックする。
  • `dbEditOptimistic` (False) : 楽観的排他制御
  • 編集開始時にはロックを取得せず、データを書き込む(`Update` メソッドを実行する)瞬間のみ、他者による改ざんがなかったかを検証する。

シニアエンジニアが理解すべきは、「どちらが優れているか」ではなく「どこで破綻するか」である。

2. 悲観的排他制御(`LockEdits = True`)の暗黒面と正しいユースケース

挙動のメカニズム

悲観的排他制御は、データ整合性の担保においては最も強固である。`Edit` を実行した瞬間にページロック(またはレコードロック)が走り、他のユーザーは `Update` や `Delete` を行うことができなくなる。

現場で直面する致命傷

しかし、これをWAN環境や、ユーザーがフォームを開いたまま離席するようなレガシー環境で用いると、システムは機能不全に陥る。
さらに悪質なのは、「VBAコードの記述順序」を誤った場合の無限待機(ハングアップ)だ。

‘ 【アンチパターン】悲観的ロックの誤用によるハングアップ誘発
Sub DangerousUpdate()
Dim db As DAO.Database
Dim rs As DAO.Recordset

Set db = CurrentDb
Set rs = db.OpenRecordset(“T_OrderMaster”, dbOpenDynaset)

rs.FindFirst “OrderID = ‘ORD-202X-001′”
If Not rs.NoMatch Then
‘ 悲観的ロックを明示的に指定
rs.LockEdits = True
rs.Edit ‘ <-- ここで他者がロックしていれば、ここでスレッドが永久ブロックされる rs!Status = "Processing" ' [悪夢]ここで外部API呼び出しや、重い処理を挟む ' この間、DBのレコードは完全に占有され続け、他の全ユーザーが排除される Call HeavyApiProcess() rs.Update End If rs.Close Set rs = Nothing Set db = Nothing End Sub

【極限の知見】悲観的ロックを使うべき唯一の領域

悲観的ロックは、「極めて短時間で完結するバッチ処理、かつ絶対に競合を許さない排他カウンターのインクリメント」など、人間が介在しない閉じたスコープでのみ使用すべきである。UI層(フォーム)での操作にこれを適用することは、アーキテクチャ上の罪悪と心得よ。

3. 楽観的排他制御(`LockEdits = False`)と「書き込み競合(Write Conflict)」の極意

現代の業務システムにおける標準解は、圧倒的に楽観的排他制御である。
データ競合の確率が低いという前提に立ち、パフォーマンスを極限まで高める。

楽観的ロックの宿命:Run-time error 3197

楽観的ロックにおいて、Aさんがレコードを読み込み、編集している間に、Bさんが同じレコードを先に更新(`Update`)してしまったとする。その後、Aさんが `rs.Update` を実行した瞬間、Accessエンジンは次世代の例外を発生させる。

> 実行時エラー ‘3197’:
> データの変更中のため、ほかのユーザーまたはプロセスによってデータが変更されました。

このエラーをハンドリングできないシステムは、本番環境でユーザーの信頼を失う。

プロフェッショナルなエラーハンドリング実装例

以下のコードは、楽観的ロックにおける競合を検知し、最新のデータベース上の値と比較・再試行(リトライ)を行う実用的なアーキテクチャのテンプレートである。

Sub RobustOptimisticUpdate(ByVal targetID As String, ByVal newStatus As String)
Dim db As DAO.Database
Dim rs As DAO.Recordset
Dim retryCount As Integer
Const MAX_RETRIES As Integer = 3

Set db = CurrentDb
retryCount = 0

RetryLoop:
On Error GoTo ErrorHandler

‘ ダイナセットを開く
Set rs = db.OpenRecordset(“SELECT FROM T_OrderMaster WHERE OrderID = ‘” & targetID & “‘”, dbOpenDynaset)

If rs.EOF Then
MsgBox “対象レコードが存在しません。”, vbCritical
GoTo Cleanup
End If

‘ 楽観的ロックの設定(デフォルトだが明示的に記述)
rs.LockEdits = False

rs.Edit
rs!Status = newStatus
‘ タイムスタンプやバージョンカラムのインクリメント(存在する場合)
If FieldExists(rs, “VersionNo”) Then
rs!VersionNo = Nz(rs!VersionNo, 0) + 1
End If

‘ ここで競合が発生していればエラー3197が飛ぶ
rs.Update

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

ErrorHandler:
If Err.Number = 3197 Then
‘ 3197: 書き込み競合エラー
retryCount = retryCount + 1
If retryCount <= MAX_RETRIES0 Then ' オブジェクトを解放してリトライ rs.Close Set rs = Nothing ' バックオフ(数ミリ秒待機するなどの配慮も有効だが、簡易的に再クエリ) DoEvents Resume RetryLoop Else MsgBox "他のユーザーによってデータが更新されています。" & vbCrLf & _ "変更内容を破棄し、画面を再読み込みしてください。", 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 ' ヘルパー関数: フィールドの存在確認 Private Function FieldExists(rs As DAO.Recordset, fieldName As String) As Boolean Dim fld As DAO.Field On Error Resume Next Set fld = rs.Fields(fieldName) FieldExists = (Err.Number = 0) On Error GoTo 0 End Function ---

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

Access VBAにおける最大のパフォーマンス劣化要因は、オブジェクトの解放漏れと、不必要なスコープでの `CurrentDb` の乱用だ。

特に `Recordset` を操作する際は、以下の鉄則を死守しなければならない。

1. `CurrentDb` は変数にキャッシュせよ
`CurrentDb` を呼び出すたびに、Accessは内部で新しいDatabaseオブジェクトのインスタンスを生成し、システムリソースを消費する。必ず一度変数に格納し、処理の最後で `Set db = Nothing` で解放すること。
2. `Recordset` は使い捨て、即座に破棄せよ
メモリ上に常駐させる必要がない限り、関数やプロシージャのスコープ内で生成し、終わったら確実に `.Close` し、`Set rs = Nothing` を行うこと。これを怠ると、Accessの内部メモリリーク(Jet/ACEのキャッシュ肥大化)を引き起こし、数日間稼働しただけでアプリケーションがクラッシュする原因となる。

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

Access VBAは「おもちゃの言語」ではない。正しくアーキテクチャを理解し、ハードウェアとデータベースエンジンの挙動を掌握すれば、ミッションクリティカルな現場のワークホースとして十全に機能する。

  • UI(フォーム)での直接バインドによる編集 は、内部的に楽観的ロックと悲観的ロックの境界が曖昧になり、予期せぬロック競合を生む。大規模なマルチユーザー環境では、フォームは非バインド(Unbound)で構築し、今回解説したような `DAO.Recordset` による明示的なトランザクション制御と楽観的ロックをコードで実装せよ。

この境地に到達したとき、あなたの書くVBAコードは、もはや単なるスクリプトではなく、堅牢なエンタープライズ・ソフトウェアの挙動を獲得する。

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