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

スポンサーリンク

Access VBAを掌握する極限の知見:DAO.Recordsetの「LockEdits」が握るトランザクション制御の真実

データベースエンジニアリングの領域において、排他制御(Concurrency Control)の設計はシステムの生死を分ける。特に、Jet/ACEデータベースエンジンを基盤とするMicrosoft Accessにおいて、VBAから操作する `DAO.Recordset` のロック機構を正しく理解し、制御できている開発者は驚くほど少ない。

「とりあえず `CurrentDb.OpenRecordset` で開いておけば動く」という甘い認識は、多端末が同時に稼働する現場でデータベースの破損(Lock-file corruption)、デッドロック、あるいは深刻なパフォーマンス低下を引き起こす。

今回は、DAOの `LockEdits` プロパティに焦点を当て、悲観的排他制御(Pessimistic Locking)楽観的排他制御(Optimistic Locking) の本質的な違い、そして現場の要件に合わせた極限の使い分けを、アーキテクトの視点から解説する。

1. 悲観的排他制御 vs 楽観的排他制御のメカニズム

DAOにおけるレコードロックの挙動は、`Recordset` オブジェクトを開く際、あるいは `Edit` メソッドを呼び出した瞬間に決定される。これを司るのが `LockEdits` プロパティである。

| 項目 | 悲観的排他制御 (`LockEdits = True`) | 楽観的排他制御 (`LockEdits = False`) |
| :— | :— | :— |
| ロック獲得のタイミング | `Edit` メソッド実行時 | `Update` メソッド実行時 |
| データ競合の考え方 | 「他のユーザーは必ず書き換える」 | 「めったに競合は起きない」 |
| ロック保持期間 | `Edit` から `Update`(または `Close`)まで | 書き込みの瞬間(マイクロ秒単位)のみ |
| 操作性 (UX) | 他者の作業をブロックするため、極めて悪い | ブロックしないため良好だが、競合時に例外 |
| 主な用途 | 金融勘定や在庫引き当てなど厳密な整合性が必要な場合 | 一般的なマスターメンテナンスや大量バッチ処理 |

悲観的排他制御の罠

`LockEdits = True`(既定値のことが多い)を設定した場合、`rs.Edit` を実行した瞬間から、Jetエンジンは対象レコードを含む4バイト(または1ページ)の領域を物理的にロックする。
この状態でユーザーがフォーム上で入力の手を止めたり、長考に入ったりすると、他の全セッションからの書き込みが完全にブロックされる。多人数が同時アクセスする基幹システムにおいて、悲観的ロックの多用は「システムの硬直」を招く最大の要因となる。

2. 実践:VBAにおける `LockEdits` の明示的制御とメモリ最適化

プロフェッショナルなコードベースでは、暗黙的な設定に依存せず、要件に応じて `OpenRecordset` のオプションと `LockEdits` をコードで明示的に制御する。

以下のコードは、厳密な在庫引き当て処理において「悲観的排他制御」を適用し、かつオブジェクトのライフサイクルを完全に管理(メモリリークの根絶)した実装例である。

Option Explicit

‘ ==============================================================================
‘ 担当者: シニアチーフアーキテクト
‘ 概要: 悲観的排他制御を用いた厳密な在庫引き当て処理
‘ ==============================================================================
Public Function ExecuteStrictInventoryAllocation(ByVal targetProductID As Long, ByVal requiredQty As Long) As Boolean
Dim db As DAO.Database
Dim rs As DAO.Recordset
Dim strSQL As String

ExecuteStrictInventoryAllocation = False

‘ 現在のデータベース参照を取得(CurrentDbの乱用を避け、変数にキャッシュ)
Set db = CurrentDb()

‘ トランザクションの開始
db.BeginTrans

On Error GoTo ErrorHandler

‘ 【重要】DBAの知見:
‘ 悲観的ロックを行う場合、OpenRecordsetの時点でdbDenyWriteやdbSeeChangesを
‘ 適切に組み合わせ、競合検知の精度を高める。
strSQL = “SELECT ProductID, StockQuantity, LastUpdated FROM T_Inventory WHERE ProductID = ” & targetProductID

‘ dbOpenDynaset を指定し、ロックを有効化してレコードセットを開く
Set rs = db.OpenRecordset(strSQL, dbOpenDynaset, dbSeeChanges)

If rs.EOF Then
Err.Raise 9999, “Inventory”, “指定された商品IDが存在しません。”
End If

‘ 編集モードへの移行(ここで悲観的ロックが発動する)
rs.Edit

‘ 悲観的排他制御を明示的に指定
rs.LockEdits = True

‘ 在庫数の検証と引き当て
If rs.Fields(“StockQuantity”).Value < requiredQty Then Err.Raise 9998, "Inventory", "在庫数が不足しています。" End If ' 値の更新 rs.Fields("StockQuantity").Value = rs.Fields("StockQuantity").Value - requiredQty rs.Fields("LastUpdated").Value = Now() ' 更新の確定(この瞬間にロックが解放される) rs.Update ' トランザクションのコミット db.CommitTrans ExecuteStrictInventoryAllocation = True GoTo CleanUp ErrorHandler: ' 異常系:ロールバックの実行 On Error Resume Next db.Rollback MsgBox "在庫引き当てに失敗しました: " & Err.Description, vbCritical, "排他制御エラー" CleanUp: ' 【極限のメモリ最適化】 ' DAOオブジェクトは明示的にCloseし、Nothingを代入してメモリプールを解放する。 ' これを怠ると、AccessのRAM消費量が肥大化し、Jetエンジンのキャッシュ汚染を引き起こす。 If Not rs Is Nothing Then rs.Close Set rs = Nothing End If Set db = Nothing End Function ---

3. 楽観的排他制御(Optimistic Locking)の実装と競合ハンドリング

現代のWebアプリケーションや、ユーザーが画面を開いたまま離席するような業務システムでは、楽観的排他制御 (`LockEdits = False`) がデファクトスタンダードである。

楽観的ロックでは、レコードを開いている間は誰もロックしない。書き込み(`Update`)の瞬間だけレコードを再読込し、「他のユーザーに先に書き換えられていないか」を検証する。もし書き換えられていれば、DAOは実行時にランタイムエラー(例:エラー番号 3189 または 3260 「別のユーザーがデータを変更しました」)を発生させる。

この競合をコードレベルでトラップし、ユーザーに適切な選択肢(上書き保存か、再読込か)を提供することが、シニアエンジニアの腕の見せ所である。

‘ ==============================================================================
‘ 概要: 楽観적排他制御を用いたマスターデータ更新と競合ハンドリング
‘ ==============================================================================
Public Sub UpdateCustomerMaster(ByVal customerID As Long, ByVal newCompanyName As String)
Dim db As DAO.Database
Dim rs As DAO.Recordset
Const CONFLICT_ERROR As Long = 3189 ‘ 競合エラーの代表例 (環境により3260等も考慮)

Set db = CurrentDb()

On Error GoTo OptimisticErrorHandler

Set rs = db.OpenRecordset(“SELECT CustomerID, CompanyName, UpdateCounter FROM T_Customers WHERE CustomerID = ” & customerID, dbOpenDynaset, dbSeeChanges)

If Not rs.EOF Then
rs.Edit
‘ 楽観的ロックの明示
rs.LockEdits = False

rs.Fields(“CompanyName”).Value = newCompanyName
‘ 更新カウンター(楽観排他用のタイムスタンプやバージョン列)をインクリメント
If rs.Fields(“UpdateCounter”.Name) <> “” Then
rs.Fields(“UpdateCounter”).Value = Nz(rs.Fields(“UpdateCounter”).Value, 0) + 1
End If

‘ 更新実行(ここで競合があればトラップされる)
rs.Update
MsgBox “更新が完了しました。”, vbInformation
End If

CleanUp:
If Not rs Is Nothing Then rs.Close: Set rs = Nothing
Set db = Nothing
Exit Sub

OptimisticErrorHandler:
If Err.Number = CONFLICT_ERROR Or Err.Number = 3260 Then
‘ 【アーキテクトの知見】
‘ 競合発生時は、ローカルの変更を破棄するか、強制上書きするかを判断させる
Dim response As VbMsgBoxResult
response = MsgBox(“このデータは他のユーザーによって既に変更されています。” & vbCrLf & _
“最新のデータを再読み込みしますか?(「いいえ」で強制上書き)”, _
vbYesNo + vbExclamation, “排他制御競合検知”)

If response = vbYes Then
‘ 変更をキャンセルして再開
rs.CancelUpdate
MsgBox “処理をキャンセルしました。画面を再読み込みしてください。”, vbInformation
Else
‘ 強制上書きのロジック(必要に応じて再取得して値を強制適用)
‘ ※実務ではテーブル設計にタイムスタンプ型(RowVersion)を持たせるのが定石
Resume
End If
Else
MsgBox “予期せぬエラーが発生しました: ” & Err.Description, vbCritical
End If
Resume CleanUp
End Sub

4. チーフアーキテクトが警鐘を鳴らす「Access/DAO」の限界と極限チューニング

ここまで `LockEdits` のコードレベルの制御を解説したが、システム基盤のアーキテクトとして、Access (Jet/ACE) の物理的限界についても言及しておかねばならない。

1. ページロックの呪縛
DAOのレコードロックは、実はレコード単位ではなく 「4リッチ(約2KB〜4KBのページ単位)」 で行われる。そのため、同一ページ内にある全く別のレコードを別ユーザーが編集しようとした場合でも、悲観的ロックの影響を受けてウェイト(待ち状態)が発生する。高頻度アクセスのテーブルでは、テーブルの正規化を進めるか、レコードサイズを極限まで小さく設計する必要がある。
2. ネットワーク越しの `CurrentDb` の危険性
バックエンドの `.accdb` ファイルをファイルサーバー(SMB共有)に置き、複数のクライアントから直接 `CurrentDb()` 経由でトランザクションとロックを張ると、ネットワークパケットの往復とファイルロックの競合により、パフォーマンスが劇的に劣化する。本格的な同時実行性を求めるならば、バックエンドを SQL Server (Azure SQL Database 等) に移行し、AccessをADPあるいはODBC経由のフロントエンド(Pass-through Query)として再構築すべきである。

結言

技術に「なんとなく」は許されない。
`LockEdits = True` がもたらす確実性と引き換えのパフォーマンス低下、そして `LockEdits = False` がもたらすスケーラビリティと引き換えの競合リスク。このトレードオフを正確に把握し、業務要件の特性(トランザクションの頻度、データの重要度)に合わせてVBAコードで自在に制御することこそが、真に洗練されたAccessシステムを構築する唯一の道である。

コードの行間に意図を宿せ。オブジェクトのライフサイクルを支配せよ。あなたの書くコードが、システムの命運を握っている。

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