【テクニカル・上級編】【プロ】マルチユーザー環境でのテーブル定義変更を安全に行うための「排他ロック」管理 – Access VBA解析バイブル

スポンサーリンク

【プロ】マルチユーザー環境でのテーブル定義変更を安全に行うための「排他ロック」管理

アクセスVBAによる本格的なクライアント/サーバー、あるいはファイル共有型のマルチユーザーシステムにおいて、最もエンジニアの頭を悩ませる瞬間の一つが「稼働中のテーブル定義変更(DDL)」である。

「他のユーザーがデータベースを開いているため、テーブルを排他ロックできませんでした。(エラー 3216 / 3045 等)」

この残酷なエラーメッセージに直面し、力技で全ユーザーを強制切断したり、夜間バッチの時間帯に冷や汗をかきながら手動でメンテンスを行ったりした経験を持つ者は多いはずだ。
プロのアーキテクトが目指すべきは、属人化した力づくの運用ではない。VBAとDAO、そしてWindows環境のセッション制御を極限まで理解し、プログラム自身が安全に排他ロックを奪取・管理するメカニズムの構築である。

今回は、マルチユーザー環境の混沌を制御下におき、テーブル定義の動的変更を完全に自動化するための極限の知見を公開する。

—

1. なぜAccessのテーブル定義変更はこれほどまでに難易度が高いのか

Access(ACE/Jetエンジン)のアーキテクチャにおいて、`TableDef` オブジェクトを通じたスキーマ変更(フィールドの追加、データ型の変更、インデックスの再構築など)は、データベース全体に対する排他制御(Exclusive Mode)を要求する。

マルチユーザー環境では、以下の要因が排他制御の障壁となる。

  • コネクションの残存: 他のユーザーがフォームを開いている、レコードセットを保持している、あるいは単に `.accdb` に接続しているだけで、Jetエンジンはメタデータの排他ロックを拒絶する。
  • ネットワークの切断遅延: クライアントが突然PCの電源を切った場合など、LDB(またはLACCDBC)ファイルがロック情報を保持し続け、孤立したセッションがロックを居座り続ける。
  • DAOの暗黙的トランザクションとキャッシュ: VBA側でオブジェクトの解放(`Set obj = Nothing`)を怠ると、ガベージコレクションのタイミングが予測できず、いつまでもロックが解放されない。

この環境下で安全にDDLを流すためには、「現在誰が接続しているかを監視し、安全に退避・切断を促す(あるいは強制解放する)」か、あるいは「排他取得のリトライロジックとエラーハンドリングを極限まで堅牢にする」かの二択を迫られる。

—

2. 実装アプローチ:リトライ制御とコネクション管理の極意

実務において、全ユーザーを強制切断することは業務上のリスクが大きい。したがって、以下のステップを踏むプロシージャが求められる。

1. 排他モードでのDBオープン(`OpenDatabase` の排他フラグ)の試行
2. 失敗時の詳細エラーキャッチと、原因(他ユーザーの存在)の特定
3. 指定秒数のリトライ(ポーリング)機構
4. 安全な `TableDef` 操作と、メモリリークを防ぐオブジェクトの完全解放

実装コード:安全な排他テーブル定義変更エンジン

以下のコードは、指定したテーブルに対して排他ロックを取得し、安全にカラムを追加・変更するためのプロフェッショナル・テンプレートである。

Option Compare Database
Option Explicit

‘ エラー定数
Const cErrDatabaseLocked As Long = 3216 ‘ 他のユーザーによって排他ロックされています
Const cErrPermissionDenied As Long = 3045

Public Sub SafeAlterTableDefinition()
Dim db As DAO.Database
Dim tdf As DAO.TableDef
Dim fld As DAO.Field
Dim retryCount As Long
Dim maxRetries As Long
Dim waitSeconds As Long
Dim isLocked As Boolean

maxRetries = 5 ‘ 最大リトライ回数
waitSeconds = 3 ‘ リトライ間隔(秒)
isLocked = False

‘ 1. 排他モードでのデータベース接続試行ループ
For retryCount = 1 to maxRetries
On Error Resume Next
‘ CurrentDbではなく、明示的に排他モードで自DBを再オープンする
Set db = DBEngine.Workspaces(0).OpenDatabase(CurrentDb.Name, True)

If Err.Number = 0 Then
isLocked = True
On Error GoTo 0
Exit For
Else
‘ 3216や3045以外の予期せぬエラーなら即座に中断
If Err.Number <> cErrDatabaseLocked And Err.Number <> cErrPermissionDenied Then
Dim errNum As Long: errNum = Err.Number
Dim errDesc As String: errDesc = Err.Description
On Error GoTo 0
Err.Raise errNum, “SafeAlterTableDefinition”, “予期せぬエラー: ” & errDesc
End If

On Error GoTo 0
‘ ユーザーへの通知やログ出力(今回はDebug.Print)
Debug.Print “排他ロック取得失敗。(” & retryCount & “/” & maxRetries & “) ” & waitSeconds & “秒後に再試行します…”

‘ Windows APIのSleepでCPU負荷を抑えて待機
Call Sleep(waitSeconds 1000)
End If
Next retryCount

‘ 排他取得できなかった場合の処理
If Not isLocked Then
MsgBox “現在、他のユーザーがシステムを使用しているため、テーブル定義を変更できません。” & vbCrLf & _
“時間を置いて再度実行してください。”, vbCritical, “排他制御エラー”
Exit Sub
End If

‘ 2. トランザクションとテーブル定義変更 (DDL)
On Error GoTo ErrorHandler
db.BeginTrans

Set tdf = db.TableDefs(“M_ClientData”)

‘ 例:フィールドの存在チェックと追加
If Not FieldExists(tdf, “LastModifiedBy”) Then
Set fld = tdf.CreateField(“LastModifiedBy”, dbText, 50)
fld.AllowZeroLength = True
tdf.Fields.Append fld
Debug.Print “フィールド ‘LastModifiedBy’ を追加しました。”
End If

‘ 変更をコミット
db.CommitTrans
MsgBox “テーブル定義の変更が正常に完了しました。”, vbInformation, “完了”

CleanUp:
‘ 3. メモリの明示的解放(オブジェクトのライフサイクル管理)
On Error Resume Next
If Not fld Is Nothing Then Set fld = Nothing
If Not tdf Is Nothing Then Set tdf = Nothing
If Not db Is Nothing Then
db.Close
Set db = Nothing
End If
Exit Sub

ErrorHandler:
‘ ロールバックによる整合性担保
db.Rollback
MsgBox “エラーが発生したため、変更をロールバックしました。” & vbCrLf & _
“Error: ” & Err.Description, vbCritical, “致命的エラー”
Resume CleanUp
End Sub

‘ ヘルパー関数: フィールド存在チェック
Private Function FieldExists(tdf As DAO.TableDef, fieldName As String) As Boolean
Dim f As DAO.Field
FieldExists = False
For Each f in tdf.Fields
If StrComp(f.Name, fieldName, vbTextCompare) = 0 Then
FieldExists = True
Exit For
End If
Next f
Set f = Nothing
End Function

‘ 処理停止用 Windows 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

—

3. チーフアーキテクトが教える「死角なき」メモリ管理とオブジェクトの作法

上記のコードを見て、「なぜわざわざ `CurrentDb` を使わずに `DBEngine.Workspaces(0).OpenDatabase` で自分自身を排他オープンしているのか」と疑問に思った読者は鋭い。

`CurrentDb` の罠

`CurrentDb` は呼び出すたびに新しい隠しDAOコネクションを生成し、内部キャッシュを破棄する。これをマルチユーザー環境でのDDLの最中に安易に使い回すと、DAOの内部キャッシュと実際のJetエンジンのロック状態に乖離が生じ、「Ghost Lock(幽霊ロック)」と呼ばれるメモリリークを引き起こす原因になる。

オブジェクトの逆順解放の哲学

VBAのガベージコレクションは非常に頼りない。特にDAOの `Database`、`TableDef`、`Recordset`、`Field` の階層構造において、親オブジェクトを子オブジェクトより先に解放(`Set = Nothing`)すると、メモリ空間上にポインタの残骸が残り、Accessのプロセスが肥大化したりクラッシュする原因となる。

プロフェッショナルは、必ず「生成した順番とは逆の順序」で、かつエラーハンドリングのいかんを問わず確実に `Nothing` を代入する。上記の `CleanUp` ラベルはその模範である。

—

4. レガシー環境とシステム間連携における実践的トリアージ

もし、この排他ロックすら取得できないほどの「常時接続型の巨大バッチ」や「ゾンビセッション」が社内ニッチ環境に存在する場合、さらに踏み込んだアーキテクチャが必要となる。

1. LDBViewer等を用いたセッション監視:
Windows APIを用いて、該当 `.accdb` に紐づく `.ldb/.laccdbc` ファイルを解析し、接続しているホスト名やWindowsアカウントを特定してログに吐き出す仕組みを前段に挟む。
2. スプリットデータベース構造の厳守:
大前提として、テーブル定義を変更する対象は必ずバックエンド(データ側:_be.accdb)であり、ユーザーが操作するフロントエンド(UI側:_fe.accdb)ではない。UI側からバックエンドのファイルに対して直接 `OpenDatabase(…, True)` を叩くことで、フロントエンドの接続状態に邪魔されずに排他制御を完結させることができる。

—

総括

Access VBAは「おもちゃの言語」と揶揄されることがある。しかし、それは扱うエンジニアがメモリのライフサイクルやOSの排他制御の理を理解していない言い訳に過ぎない。

マルチユーザー環境におけるテーブル定義の変更は、インフラストラクチャの制御そのものである。今回解説した「排他接続のリトライ制御」「明示的かつ厳格なオブジェクトの破棄」「トランザクションによるロールバック担保」を実装に組み込むことで、Accessシステムは「壊れやすいレガシー」から「堅牢な基幹データストア」へと昇華する。

コードの端々にまでエンジニアの意思を宿せ。それが、プロフェッショナルの仕事である。

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