1. はじめに:Accessが「マルチユーザーで壊れる」という迷信の正体
「Accessは複数人で使うとすぐにデータベースが破損する、あるいはデッドロックで動かなくなる」——現場で幾度となく耳にしてきた無知ゆえの偏見である。
結論から申し上げよう。問題はAccessというシステムにあるのではない。JET/ACE(Access Database Engine)の排他制御メカニズムを理解せず、デフォルト挙動に依存したコードを垂れ流している開発者の実装精度にある。
特に、データ更新の競合が多発する業務システムにおいて、`DAO.Recordset`の`LockEdits`プロパティをどう制御し、どのようなエラーハンドリング機構を組むかは、システムの寿命を左右する極限の分岐点となる。本稿では、表層的なVBAリファレンスには絶対に書かれていない、悲観的ロック(Pessimistic Lock)と楽観的ロック(Optimistic Lock)の物理的な挙動、メモリ最適化、そしてWindows APIを用いた堅牢な排他制御の実装パターンを解体・再構築する。
—
2. JET/ACEエンジンにおける排他制御の物理メカニズム
`DAO.Recordset`でデータを更新する際、エンジン内部ではLDB/LCCDB(ロック管理ファイル)を介してページレベル、またはレコードレベルのロックフラグが高速に書き換えられている。ここで絶対に理解しておかなければならないのは、`LockEdits`プロパティが決定する「ロックの寿命(ライフサイクル)」である。
【悲観的ロック (LockEdits = True)】
.Edit 発行 ──[ ロック確保 ]───────────────> .Update 発行 ──[ ロック解除 ]
【楽観的ロック (LockEdits = False)】
.Edit 発行 ──────────────────────────────> .Update 発行 ──[ ロック確保 & 即解除 ]
悲観的ロック (`LockEdits = True`)
- 挙動: `.Edit` メソッドが呼び出された瞬間に該当レコード(または所属する4KBのページ)を物理的にロックする。
- 利点: `.Edit` が成功した時点で「更新の権利」が完全に保証される。`.Update` 実行時に他者による更新衝突(競合)が発生しない。
- リスク: ユーザーが画面上で編集を開始したまま離席した場合、そのレコードは他ユーザーから永久に更新不可となる。また、古くから存在する4KBページロックの副産物として、「編集中のレコードの隣接レコードまで巻き添えでロックされる」という重大なサイドエフェクトを引き起こす可能性がある。
楽観的ロック (`LockEdits = False`)
- 挙動: `.Edit` 時にはロックを取得せず、`.Update` メソッドが呼び出された瞬間のみロックを確保し、即座に更新して解除する。
- 利点: ロックの占有時間がミリ秒単位(またはマイクロ秒単位)に最小化されるため、マルチユーザー環境におけるスループットが劇的に向上する。
- リスク: `.Edit` から `.Update` の間に別ユーザーが同じレコードを更新してコミットした場合、エラー `3197`(データ変更競合)が発生する。これを捉えてリトライするか、ユーザーに通知するロジックが必須となる。
—
3. 「悲観的」と「楽観的」の技術的選定基準
シニアエンジニアとしてアーキテクチャを設計する際、感覚でロック戦略を選んではならない。以下のパラメータに基づき、厳密に使い分ける必要がある。
| 評価軸 | 悲観的ロック (`LockEdits = True`) | 楽観的ロック (`LockEdits = False`) |
| :— | :— | :— |
| 主用途 | バッチ処理、整合性が最優先される基幹処理 | OLTP風の画面入力、高並列参照環境 |
| 競合発生時の挙動 | `.Edit` 呼び出し時点でエラー(他者がロック中) | `.Update` 呼び出し時点でエラー(他者が変更済) |
| ネットワーク負荷 | 高(ロック維持のためのLDB通信が発生) | 極めて小(瞬時のI/Oのみ) |
| リトライ実装 | シンプル(`.Edit` の可否判定のみ) | 高度(衝突検出時の自動マージまたは再試行) |
結論として、現代の高速かつ高並列なVBAシステム開発においては、「原則として楽観的ロック (`LockEdits = False`) を採用し、エラー `3197` をWindows APIベースのミリ秒単位バックオフアルゴリズムで吸収する」のが最も美しい解法となる。
—
4. 極限の排他制御パターン:Windows API連携とリトライエンジン
以下に、現場でそのままコピペしてプロダクション環境に投入できるレベルの堅牢なDAO更新処理を提示する。単に `LockEdits` を切り替えるだけでなく、以下の要素を組み込んである。
1. Windows API (`Sleep`) による精密な再試行待機(VBAの `DoEvents` や `Timer` ループによるCPUリソース枯渇の回避)
2. `CurrentDb` インスタンスの反復呼び出し回避(メモリリークと速度低下の撲滅)
3. トランザクション (`Workspace`) との完全同期
4. 明示的なオブジェクト解放(`Set Nothing`)による参照カウンタの完全リセット
Option Explicit
‘ —————————————————————————–
‘ Windows APIの宣言
‘ CPU使用率を無駄に跳ね上げずに、ミリ秒単位でスレッドを非同期待機させるための必須API
‘ —————————————————————————–
If VBA7 Then
Private Declare PtrSafe Sub Sleep Lib “kernel32” (ByVal dwMilliseconds As Long)
Else
Private Declare Sub Sleep Lib “kernel32″ (ByVal dwMilliseconds As Long)
End If
‘ —————————————————————————–
‘ エラーコード定義 (JET/ACE Engine)
‘ —————————————————————————–
Private Const ERROR_DATA_CHANGED As Long = 3197 ‘ 楽観的ロック競合:参照後に他者がデータを変更した
Private Const ERROR_RECORD_LOCKED As Long = 3260 ‘ 悲観的ロック競合:他者がレコードをロック中
Private Const ERROR_TABLE_LOCKED As Long = 3211 ‘ テーブル全体がロック中
”’
”’
”’ 更新対象のID
”’ 更新データ
”’
Public Function SafeUpdateRecord(ByVal targetID As Long, ByVal newValue As String) As Boolean
On Error GoTo ErrorHandler
Dim ws As DAO.Workspace
Dim db As DAO.Database
Dim rs As DAO.Recordset
Dim strSQL As String
Dim retryCount As Integer
Const MAX_RETRIES As Integer = 5 ‘ 最大再試行回数
Const RETRY_INTERVAL_MS As Long = 100 ‘ リトライ間隔 (100ms)
SafeUpdateRecord = False
retryCount = 0
‘ 1. WorkspaceとDatabaseの参照を明示的に固定(CurrentDbの直接連打は厳禁)
Set ws = DBEngine(0)
Set db = CurrentDb
strSQL = “SELECT T_ID, F_Value, F_UpdatedAt FROM T_Master WHERE T_ID = ” & targetID
RetryPoint:
‘ 2. ダイナセット形式で開く(Update可能な状態を維持)
‘ dbSeeChanges はSQL Server連携時等のIDENTITYカラム対策として常に付与するのが作法
Set rs = db.OpenRecordset(strSQL, dbOpenDynaset, dbSeeChanges)
If rs.EOF And rs.BOF Then
‘ 対象レコードが存在しない
GoTo Cleanup
End If
‘ ————————————————————————-
‘ 3. 排他制御の中核設定:楽観的ロックを指定
‘ ————————————————————————-
rs.LockEdits = False ‘ False = 楽観的ロック (Update時のみロック)
‘ トランザクションの開始(ACID属性の確保)
ws.BeginTrans
‘ 編集モードの開始(この時点ではロックされない)
rs.Edit
‘ フィールド値の更新
rs.Fields(“F_Value”).Value = newValue
rs.Fields(“F_UpdatedAt”).Value = Now()
‘ 更新の確定(ここで物理ロックが瞬時取得され、競合判定が行われる)
rs.Update
‘ トランザクションコミット
ws.CommitTrans
‘ 成功
SafeUpdateRecord = True
Cleanup:
‘ 4. リソースの完全明示解放(ガベージコレクションへの依存を断ち切る)
On Error Resume Next
If Not rs Is Nothing Then rs.Close: Set rs = Nothing
If Not db Is Nothing Then Set db = Nothing
If Not ws Is Nothing Then Set ws = Nothing
Exit Function
ErrorHandler:
‘ トランザクション発行中であればロールバック
If Not ws Is Nothing Then
‘ トランザクション状態の安全性チェック(エラー時の巻き戻し)
On Error Resume Next
ws.Rollback
On Error GoTo ErrorHandler
End If
Dim errNum As Long
errNum = Err.Number
‘ ————————————————————————-
‘ 5. ロック競合エラーハンドリングと指数バックオフ的再試行
‘ ————————————————————————-
If (errNum = ERROR_DATA_CHANGED Or errNum = ERROR_RECORD_LOCKED Or errNum = ERROR_TABLE_LOCKED) Then
retryCount = retryCount + 1
If retryCount <= MAX_RETRIES Then ' オブジェクトの安全なリセット If Not rs Is Nothing Then rs.Close: Set rs = Nothing ' Windows APIによるCPU非負荷待機(ジッターを加えて衝突を分散) Call Sleep(RETRY_INTERVAL_MS retryCount) ' 再試行実行 DoEvents ' イベントループの開放(フリーズ防止) Resume RetryPoint Else ' リトライ上限超過 MsgBox "他ユーザーによる更新が集中しているため、処理を完了できませんでした。" & vbCrLf & _ "時間をおいて再度お試しください。(Error: " & errNum & ")", vbCritical, "排他制御エラー" End If Else ' 想定外の不測のエラー MsgBox "予期せぬシステムエラーが発生しました: " & Err.Description & " (" & errNum & ")", vbCritical End If GoTo Cleanup End Function ---
5. シニアエンジニアが刻むべきメモリとオブジェクトライフサイクルの鉄則
上記のコードから、本質的なパフォーマンス最適化のポイントを3つ抽出して解説する。これを徹底しない限り、どんなに完璧なロックロジックを書こうがシステムは崩壊する。
① `CurrentDb` のアンチパターンを排除せよ
VBA初心者は平気で `CurrentDb.OpenRecordset(…)` と書く。これは呼び出されるたびに隠蔽されたDatabaseオブジェクトのコピーを生成し、メモリ上に孤立させる。ループ処理や頻繁なデータアクセスのなかでこれを行うと、参照カウンタが未解放のまま残り、LDBファイルが肥大化し、最終的に「リソース不足」でクラッシュする。
必ず `Set db = CurrentDb` と変数に代入し、処理が終わったら `Set db = Nothing` で解放しなければならない。
② `LockEdits` 設定のタイミング
`LockEdits` プロパティの変更は、必ず `.Edit` メソッドを呼び出す前 に実行しなければならない。`.Edit` を呼んだ後に `LockEdits` を変更しても、すでにJETエンジン側で確保されたロックメカニズムを変更することはできず、実行時エラーを引き起こす。
③ エラー `3197` 発生時のオブジェクト破棄
楽観的ロックで `.Update` 時に `3197`(データ変更競合)が発生した場合、そのRecordsetオブジェクトの内部バッファは既に汚染されている。そのまま再度 `.Update` や `.Edit` を試みても正常復帰しないことが多い。コード例に示してある通り、一度 `rs.Close` を実行して `Set rs = Nothing` を行い、クエリを再発行して最新のレコード状態をDBから取り直した上でリトライしなければならない。
—
6. おわりに:アーキテクトとしての結論
Access VBAにおける排他制御とは、単なる「エラー処理の追加」ではない。それは JET/ACEエンジンの内部動作、Windowsのマルチスレッド/ファイルロック機構、そしてVBAのメモリ空間管理を統合して設計する高度なシステムアーキテクチャ である。
- 原則として `LockEdits = False` (楽観的ロック) をベースに設計せよ。
- 衝突は必ず発生するものと定義し、Windows API (`Sleep`) を伴う自動リトライ機構を組み込め。
- `CurrentDb` や `Recordset` の ライフサイクル(明示的解放)をミリ秒単位で管理 せよ。
これらを徹底することで、Accessシステムは「複数人で動かすと壊れる玩具」から、「数千人規模のエンタープライズ環境の末端で何年間も無停止で動く堅牢なフロントエンド」へと昇華する。レガシーを現代の技術精度で掌握することこそ、我々チーフアーキテクトに課せられた使命である。
