【プロ】マルチユーザー環境でのテーブル定義変更に伴う排他制御の極意
Accessによるクライアント・サーバー(あるいはファイル共有型)のマルチユーザーシステムにおいて、最も忌むべき事態は何か。それは、稼働中の本番環境において、他ユーザーがレコードを掴んでいるがために発生する「実行時エラー ‘3211’: データベースを排他的に使用できる状態にできません」の悲鳴だ。
シニアエンジニアであれば、DAOの`TableDef`や`Field`オブジェクトを操作して動的にスキーマを変更しようとした矢先、このエラーに阻まれた経験が一度はあるはずだ。
本稿では、数多のレガシーシステムを死地から救ってきたチーフアーキテクトの視点から、マルチユーザー環境におけるテーブル定義変更の極限の知見を公開する。DAOのオブジェクトライフサイクル管理、Windows APIを用いたプロセス間同期、そしてトランザクションとエラーハンドリングの極致をここに記す。
—
1. なぜテーブル定義の変更はこれほどまでに重いのか
まず、Access(ACE/Jetエンジン)の内部挙動を理解しなければならない。
データを操作するDML(`SELECT`, `INSERT`等)はレコード単位、あるいはページ単位のロックで協調動作するが、テーブル構造を変更するDDL(`TableDef.Append`や`Field.CreateField`等)は、そのテーブルに対する完全な排他ロック(Exclusive Lock)を要求する。
マルチユーザー環境において、たった1人のユーザーがフォームで対象テーブルのレコードを開いているだけで、ACEエンジンはスキーマの書き換えを拒絶する。さらに、VBAコード内で不適切なオブジェクト参照が残っていると、ガベージコレクションが走る前にメモリリークを引き起こし、`.accdb`ファイルそのものがロックされる原因となる。
これを突破するためには、以下の3つの要件を同時に満たす必要がある。
1. 他接続ユーザーの強制切断・あるいは安全な待機(リトライ戦略)
2. DAOオブジェクトの完全な明示的解放(メモリ最適化)
3. トランザクション分離とフォールバック機構
—
2. 【実践】排他制御を極めたテーブル定義変更モジュール
以下のコードは、マルチユーザー環境下での安全なフィールド追加を行うプロダクション品質のVBAコードである。単なるエラートラップにとどらず、再試行メカニズムと厳格なオブジェクト解放を実装している。
Option Explicit
‘ 処理の途中で強制終了した場合のメモリリークを防ぐため、必ずOption Explicitを記述する
Public Sub SafeAddTableField(ByVal strTableName As String, ByVal strFieldName As String, ByVal intFieldType As Integer, Optional ByVal lngSize As Long = 0)
Dim dbs As DAO.Database
Dim tdf As DAO.TableDef
Dim fld As DAO.Field
Dim lngRetryCount As Long
Const MAX_RETRIES As Long = 5
Const RETRY_INTERVAL_SEC As Long = 3 ‘ 3秒待機
‘ データベースオブジェクトの取得
Set dbs = CurrentDb()
On Error GoTo ErrorHandler
ReTryBlock:
‘ 【重要】データベースのスキーマキャッシュを更新
dbs.TableDefs.Refresh
‘ TableDefオブジェクトの取得(ここで排他ロックの取得を試みる)
Set tdf = dbs.TableDefs(strTableName)
‘ フィールドの存在チェック
If FieldExists(tdf, strFieldName) Then
MsgBox “指定されたフィールドは既に存在します。”, vbInformation, “スキップ”
GoTo CleanUp
End If
‘ フィールドオブジェクトの生成
If lngSize > 0 Then
Set fld = tdf.CreateField(strFieldName, intFieldType, lngSize)
Else
Set fld = tdf.CreateField(strFieldName, intFieldType)
End If
‘ テーブル定義への追加(この瞬間に排他ロックが必須となる)
tdf.Fields.Append fld
tdf.Fields.Refresh
MsgBox “テーブル定義の変更に成功しました。”, vbInformation, “完了”
GoTo CleanUp
ErrorHandler:
‘ エラー番号 3211: 排他ロック取得失敗、または 3042: タイムアウト等
If Err.Number = 3211 Or Err.Number = 3042 Then
lngRetryCount = lngRetryCount + 1
If lngRetryCount <= MAX_RETRIES Then
' オブジェクトを一旦解放してリトライに備える
Set fld = Nothing
Set tdf = Nothing
' ユーザーへの通知とウェイト(Windows APIのSleepを使用)
LogEvent "テーブル '" & strTableName & "' は他ユーザーによって占有されています。リトライ " & lngRetryCount & "/" & MAX_RETRIES & "..."
' VBAのSleep関数(API宣言)を用いてCPU負荷を抑えながら待機
Call Sleep(RETRY_INTERVAL_SEC 1000)
Resume ReTryBlock
Else
MsgBox "最大リトライ回数を超過しました。現在、他のユーザーがこのテーブルを使用しているため変更できません。", vbCritical, "排他制御エラー"
End If
Else
MsgBox "予期せぬエラーが発生しました: " & Err.Description, vbCritical, "エラー"
End If
CleanUp:
' 【極めて重要】オブジェクト変数の完全な解放
' DAOオブジェクトの参照を残したままにすると、ACEエンジンのロックが解除されない
On Error Resume Next
If Not fld Is Nothing Then Set fld = Nothing
If Not tdf Is Nothing Then Set tdf = Nothing
If Not dbs Is Nothing Then
dbs.Close
Set dbs = Nothing
End If
On Error GoTo 0
End Sub
' 補助関数:フィールド存在確認
Private Function FieldExists(ByVal tdf As DAO.TableDef, ByVal strFieldName As String) As Boolean
Dim f As DAO.Field
FieldExists = False
For Each f In tdf.Fields
If StrComp(f.Name, strFieldName, vbTextCompare) = 0 Then
FieldExists = True
Exit For
End If
Next f
Set f = Nothing
End Function
' 簡易ログ出力関数
Private Sub LogEvent(ByVal strMessage As String)
Debug.Print "[" & Now & "] " & strMessage
End Sub
---
3. メモリ最適化とオブジェクトのライフサイクル管理
VBA初学者が犯す最大の過ちは、「`Set`しっぱなし」のコードを書くことだ。
DAOオブジェクト(`Workspace`, `Database`, `TableDef`, `Recordset`等)は、背後でCOM(Component Object Model)コンポーネントとして動作しており、参照カウントによって管理されている。
破滅へのシナリオ
ループ内やエラーハンドリングが不十分なコードで `TableDef` を何度も取得し直すと、ACEエンジン内部のメモリ空間(Heap)が断片化し、最終的に「リソースが不足しています(Error 3035)」や、最悪の場合はアプリケーションの強制終了を引き起こす。
プロフェッショナルの鉄則
1. 逆順の解放(LIFO原則): 取得した順番の逆(`Field` -> `TableDef` -> `Database`)で確実に `Nothing` を代入する。
2. `CurrentDb()` の多用を避ける: `CurrentDb()` は呼び出すたびに新しいDatabaseオブジェクトのインスタンスを生成し、内部キャッシュを消費する。テーブル定義を変更するような高負荷な処理では、一度変数に格納し、そのインスタンスを使い回した上で最後に明示的に `Close` しなければならない。
—
4. レガシー環境・ネットワーク共有(WAN/LAN)における極限の知見
Accessバックエンドファイルがファイルサーバー(NASやWindows共有フォルダ)上に置かれている場合、SMBプロトコルのキャッシュ機構が排他制御の邪魔をすることがある。
1. ネットワーク切断・オプティロック(Opportunistic Locking)の罠
Windowsのファイル共有機能(SMB)は、パフォーマンス向上のためにクライアント側でファイルをキャッシュ(OpLock)する。これが原因で、「実際には誰も使っていないはずなのに、ACEエンジンが排他ロックを取得できない」という現象が発生する。
対策:
テーブル定義を変更するようなメンテナンススクリプトを実行する際は、事前にネットワーク上の他のアクティブユーザーに対し、アプリケーションの完全終了を促すアナウンス(フラグファイルを用いたポーリング機構など)を実装し、物理的なコネクションをゼロにすることが大前提となる。
2. Windows APIによるスマートな待機処理
VBA標準の `DoEvents` を伴うループはCPU使用率を100%に張り付かせる悪手である。以下のAPI宣言をモジュールの先頭に記述し、システムリソースに優しいウェイトを実現する。
If VBA7 Then
Public Declare PtrSafe Sub Sleep Lib “kernel32” (ByVal dwMilliseconds As Long)
Else
Public Declare Sub Sleep Lib “kernel32” (ByVal dwMilliseconds As Long)
End If
—
5. 総括
Access VBAにおけるテーブル定義の動的変更は、一歩間違えばデータベースの破損(Corruption)を招く危険な領域である。しかし、排他制御のメカニズムを理解し、適切なエラーハンドリングと厳格なオブジェクトライフサイクル管理(メモリの解放)を徹底すれば、マルチユーザー環境であっても堅牢な自動マイグレーションシステムを構築することが可能だ。
「動けばいい」という妥協を捨て、底知れぬ安定性を誇るコードベースを構築することこそが、真のプロフェッショナルエンジニアの仕事である。
