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

スポンサーリンク

黎明期からの静かなる闘争:DAO.Recordsetにおける排他制御の深淵

Microsoft Accessというプロダクトを「単なるデスクトップデータベース」と侮る者は、マルチユーザー環境における真のコンカレンシー(並行性)制御の難所に突き当たったことがない素人と言える。

Jet/ACEデータベースエンジンは、その誕生から今日に至るまで、ファイル共有ベースのアーキテクチャという宿命を背負い続けてきた。SQL Serverのような洗練されたロックマネージャーが存在しない世界で、我々エンジニアがいかにしてデータの整合性を担保し、デッドロックの泥沼を回避するか。その鍵を握るのが、`DAO.Recordset` オブジェクトにおける `LockEdits` プロパティだ。

本稿では、シニアアーキテクトの視点から、悲観的排他制御(Pessimistic)と楽観的排他制御(Optimistic)の使い分け、そして実務に耐えうる極限の実装コードを詳解する。

1. 排他制御の二大流派:その本質を見極める

`LockEdits` プロパティには、`True`(悲観的)と `False`(楽観的)の二つの顔がある。これを「なんとなく」で選ぶことは、システムの崩壊を意味する。

悲観的排他制御 (Pessimistic Locking)

  • 挙動: `.Edit` メソッドを呼び出した瞬間に、そのレコード(あるいはページ)をロックする。
  • 真髄: 「自分が編集している間は、誰にも触らせない」という強い意志。
  • 欠点: ユーザーが編集画面を開いたまま離席した場合、そのリソースは解放されず、他ユーザーの処理を完全に停止させる。

楽観的排他制御 (Optimistic Locking)

  • 挙動: `.Edit` 時にはロックをかけず、`.Update` メソッドを実行する瞬間にのみ競合をチェックする。
  • 真髄: 「競合は滅多に起きない」という前提に立ち、スループットを最大化する。
  • 欠点: 最後に保存しようとした者が、他人の変更による「書き込み競合(エラー3197)」の洗礼を受ける。

2. 現場を救う実装:エラーハンドリングとRetryロジック

DAOを用いた開発において、エラー処理を伴わない更新ロジックは、ただの「希望的観測」に過ぎない。特にマルチユーザー環境では、エラー番号 3260 (レコードロック中)3197 (他ユーザーによるデータ変更) をいかにエレガントに捌くかがエンジニアの腕の見せ所だ。

以下に、Windows APIを利用した高精度なウェイト処理を含む、プロフェッショナル向けの実装例を示す。

実装コード:悲観的ロックによる堅牢な更新処理

Option Compare Database
Option Explicit

‘ Windows APIによるスレッドのスリープ。DoEventsと組み合わせ、UIをフリーズさせずに待機。
If VBA7 Then
Private Declare PtrSafe Sub Sleep Lib “kernel32” (ByVal dwMilliseconds As LongPtr)
Else
Private Declare Sub Sleep Lib “kernel32″ (ByVal dwMilliseconds As Long)
End If

”’

”’ 悲観的ロックを用いた安全なレコード更新
”’

Public Sub SafeUpdatePessimistic(ByVal recordID As Long)
Dim db As DAO.Database
Dim rs As DAO.Recordset
Dim retryCount As Integer
Const MAX_RETRIES As Integer = 5

Set db = CurrentDb
‘ ダイナセット形式でレコードセットをオープン
Set rs = db.OpenRecordset(“SELECT FROM T_Transaction WHERE ID = ” & recordID, dbOpenDynaset)

‘ 悲観的ロックを明示的に指定
rs.LockEdits = True

On Error GoTo ErrorHandler

RetryEdit:
rs.Edit

‘ — データの書き換え処理 —
rs!UpdateTimestamp = Now()
rs!Status = “PROCESSED”
‘ ————————–

rs.Update

CleanUp:
‘ ライフサイクルの管理:明示的なクローズとメモリ解放
If Not rs Is Nothing Then
rs.Close: Set rs = Nothing
End If
Set db = Nothing
Exit Sub

ErrorHandler:
Select Case Err.Number
Case 3260 ‘ レコードは他のユーザーによってロックされています
If retryCount < MAX_RETRIES Then retryCount = retryCount + 1 ' 指数関数的バックオフ(簡易版) DoEvents Sleep (retryCount 200) Resume RetryEdit Else MsgBox "他ユーザーが編集中です。時間を置いて再度お試しください。", vbCritical Resume CleanUp End If Case Else MsgBox "予期せぬエラー: " & Err.Description, vbCritical Resume CleanUp End Select End Sub ---

3. アーキテクチャの急所:ページロックと行レベルロックの闘い

Access(ACEエンジン)には、古くから「2KBページロック」という呪縛が存在する。一つのレコードをロックしたつもりが、物理的に隣接するレコードまでロックしてしまう現象だ。

現代のAccessであれば、「行レベルのロック」オプションがデフォルトで有効になっているはずだが、レガシーなバックエンド(.mdb形式など)を扱っている場合、この設定が無視されることがある。

極限の知見:
真にスケーラビリティを求めるなら、`DAO.DBEngine.Idle dbRefreshCache` を活用せよ。
マルチユーザー環境でデータが「反映されない」という不具合の多くは、エンジンのキャッシュと物理ディスクの同期ズレに起因する。書き込み直前に `dbRefreshCache` を呼び出すことで、共有メモリ上の最新の状態を強制的に読み込ませることが可能だ。

4. 楽観的ロックにおける「書き込み競合」の解法

楽観的ロック (`LockEdits = False`) を採用する場合、ユーザーへの提示が重要になる。`Err.Number = 3197` が発生した際、あなたは「上書き」させるか、「破棄」させるか、あるいは「マージ」させるか。

‘ 楽観的ロック時のエラー処理の断片
Case 3197 ‘ データが変更されています
If MsgBox(“編集中に他のユーザーがデータを更新しました。上書きしますか?”, vbYesNo) = vbYes Then
‘ 最新の値を読み込み直してから再度Updateを試みるロジックへ
rs.Bookmark = rs.Bookmark
Resume ‘ または再編集
Else
rs.CancelUpdate
Resume CleanUp
End If

5. 終わりに:オブジェクトのライフサイクルを支配する

シニアエンジニアとして、最後に強調したいのは 「リソースの明示的解放」 だ。
VBAのガベージコレクションは脆弱だ。`Recordset` や `Database` オブジェクトを `Nothing` にセットする前に、必ず `.Close` を呼ぶ。これを怠ることは、共有ロック(.ldb / .laccdb)を不必要に保持し続け、システム全体のパフォーマンスを低下させる最大の要因となる。

排他制御とは、単なるエラー回避のテクニックではない。それは、データの完全性という「システムの魂」を守るための、設計思想そのものである。

あなたが書くその一行の `.Edit` が、数千件のトランザクションを支える静謐な礎となることを願って。

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