【プロ】マルチユーザー環境でのテーブル定義変更に伴う排他制御の極意
Accessによる業務システム開発において、避けて通れない最大の壁。それが「マルチユーザー環境におけるテーブル定義の動的変更(DDL)」だ。
「運用中のデータベースで、急遽マスタテーブルにカラムを追加する必要が生じた」
「バージョンアップ用スクリプトを走らせたら、他ユーザーが接続しているせいでエラー 3211(ファイル排他エラー)が発生した」
――こんな修羅場をくぐり抜けた経験はないだろうか。
素人が書いたコードは、運悪く誰かが画面を開いているだけで容赦なくクラッシュする。プロのエンジニアであれば、この「排他制御の壁」を完璧にロジカルに支配し、現場を止めない堅牢な仕組みを構築できなければならない。
今回は、Access VBAのTableDefおよびDatabaseオブジェクトのライフサイクルを極限まで理解し、競合を華麗にいなす最高峰の排他制御テクニックを伝授する。
—
なぜ素人のコードは「排他エラー」で爆散するのか?
多くの開発者は、テーブル定義を変更する際、以下のようなナイーブなコードを書く。
‘ 【アンチパターン】絶対にやってはいけない実装
Dim db As DAO.Database
Set db = CurrentDb
‘ 運悪く他人がこのテーブルを開いているとここで即死する
db.Execute “ALTER TABLE M_Employee ADD COLUMN IsActive YESNO”, dbFailOnError
何が問題か?
Access(JET/ACEエンジン)の仕様上、`TableDef` に対する変更や `ALTER TABLE` を実行するためには、そのテーブルに対する排他ロック(Exclusive Lock)を獲得しなければならない。
他のユーザーがフォーム、クエリ、あるいは直接テーブルを開いてレコードを掴んでいる瞬間、このロック要求は拒絶され、非情な実行時エラー(通常は実行時エラー 3211:プロセスはファイルを使用中のため…)が放たれる。
プロのエンジニアは、「運が良ければ通る」ような祈祷レイヤーのコードは書かない。「ロックが取れなければ、リトライするか、美しく安全に離脱する」。これがプロフェッショナルの設計思想だ。
—
堅牢なテーブル定義変更を実現する3つの極意
マルチユーザー環境で安全にDDLを完遂するためには、以下の3ステップをコードに組み込む必要がある。
1. タイムアウト付き排他制御の確立
即座に諦めるのではなく、数秒間ロック解除を待つ(ポーリング)機構を入れる。
2. エラーハンドリングによるトランザクションの保全
予期せぬエラー時に中途半端なスキーマ変更を残さない。
3. セーフティなオブジェクト解放(メモリリークの根絶)
DAOの各オブジェクトを確実に解放し、ACEエンジンのロックファイルをクリーンに保つ。
—
【プロダクションコード】実務で使える究極のスキーマ変更モジュール
以下のコードは、実際に大規模な業務システムの自動アップデート機能などで使われる、極限まで最適化された実用コードである。そのままコピペしてプロジェクトに組み込んでほしい。
Option Compare Database
Option Explicit
‘ ==============================================================================
‘ 処理名: SafeAddColumnToTable
‘ 概要 : マルチユーザー環境を考慮し、排他ロックを安全に取得してカラムを追加する
‘ 引数 : tableName – 対象テーブル名
‘ colName – 追加するカラム名
‘ colType – DAOのデータ型 (例: dbText, dbLong, dbBoolean)
‘ colSize – サイズ (テキスト型の場合のみ有効)
‘ 戻り値: 成功時は True、失敗時は False
‘ ==============================================================================
Public Function SafeAddColumnToTable( _
ByVal tableName As String, _
ByVal colName As String, _
ByVal colType As Integer, _
Optional ByVal colSize As Long = 0) As Boolean
Dim db As DAO.Database
Dim tdf As DAO.TableDef
Dim fld As DAO.Field
Dim ws As DAO.Workspace
Dim retryCount As Integer
Const MAX_RETRIES As Integer = 5 ‘ 最大リトライ回数
Const RETRY_INTERVAL_SEC As Integer = 2 ‘ リトライ間隔(秒)
SafeAddColumnToTable = False
‘ ワークスペースの取得(トランザクション制御の基盤)
Set ws = DBEngine.Workspaces(0)
Set db = CurrentDb
‘ 1. 事前にカラムが存在するかチェック(冪等性の担保)
If CheckColumnExists(db, tableName, colName) Then
Debug.Print “[INFO] カラム ‘” & colName & “‘ は既に存在します。”
SafeAddColumnToTable = True
Exit Function
End If
‘ 2. 排他制御を伴うリトライループの開始
retryCount = 0
DoRetry:
On Error GoTo ErrorHandler
‘ 明示的な排他モードでのロックテスト(TableDefの再取得)
‘ ※CurrentDbはキャッシュを持つため、最新のスキーマとロック状態を取得するためにRefreshする
db.TableDefs.Refresh
Set tdf = db.TableDefs(tableName)
‘ フィールドの作成と追加
If colSize > 0 Then
Set fld = tdf.CreateField(colName, colType, colSize)
Else
Set fld = tdf.CreateField(colName, colType)
End If
‘ トランザクション開始(構造変更そのものはトランザクション対象外だが、安全のため括る)
ws.BeginTrans
tdf.Fields.Append fld
tdf.Fields.Refresh
ws.CommitTrans
Debug.Print “[SUCCESS] テーブル ‘” & tableName & “‘ にカラム ‘” & colName & “‘ を追加しました。”
SafeAddColumnToTable = True
GoTo CleanUp
ErrorHandler:
‘ エラー番号 3211 (ファイルが使用中です) または 3262 (他のユーザーがロックしています)
If Err.Number = 3211 Or Err.Number = 3262 Then
ws.Rollback
retryCount = retryCount + 1
If retryCount <= MAX_RETRIES Then
Debug.Print "[WARNING] テーブルがロックされています。(" & retryCount & "/" & MAX_RETRIES & ") " & RETRY_INTERVAL_SEC & "秒後に再試行します..."
' オブジェクトの解放(ロックを一度手放す)
Set fld = Nothing
Set tdf = Nothing
' 待機処理(Windows APIのSleepを使用。VBAのDoEventsでUIフリーズを防ぐ)
Call SleepWithDoEvents(RETRY_INTERVAL_SEC 1000)
Resume DoRetry
Else
MsgBox "他のユーザーがテーブルを使用しているため、スキーマの変更に失敗しました。" & vbCrLf & _
"しばらく時間を置いてから再度実行してください。", vbCritical, "排他制御エラー"
End If
Else
' 想定外のエラー
If ws.Transactions Then ws.Rollback
MsgBox "予期せぬエラーが発生しました。" & vbCrLf & _
"エラー番号: " & Err.Number & vbCrLf & _
"エラー内容: " & Err.Description, vbCritical, "システムエラー"
End If
CleanUp:
' 徹底的なオブジェクトの解放
Set fld = Nothing
Set tdf = Nothing
Set db = Nothing
Set ws = Nothing
Exit Function
End Function
' ==============================================================================
' 補助関数: カラムの存在チェック(冪等性の確保)
' ==============================================================================
Private Function CheckColumnExists(ByRef db As DAO.Database, ByVal tableName As String, ByVal colName As String) As Boolean
Dim tdf As DAO.TableDef
Dim fld As DAO.Field
CheckColumnExists = False
Set tdf = db.TableDefs(tableName)
For Each fld In tdf.Fields
If StrComp(fld.Name, colName, vbTextCompare) = 0 Then
CheckColumnExists = True
Exit For
End If
Next fld
Set fld = Nothing
Set tdf = Nothing
End Function
' ==============================================================================
' 補助プロシージャ: UIのフリーズを防ぐSleep関数
' ==============================================================================
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
Private Sub SleepWithDoEvents(ByVal milliSeconds As Long)
Dim start As Double
start = Timer
Do While (Timer - start) < (milliSeconds / 1000)
DoEvents
If (Timer - start) < 0 Then Exit Do ' 日付変更線対策
Loop
---
プロフェッショナルの設計思想:コードの解説と勘所
上記のコードには、現場で生き残るための高度な設計思想が凝縮されている。
1. 冪等性(Idempotency)の確保
`CheckColumnExists` 関数を挟んでいる点に注目してほしい。
大規模なシステムでは、インストーラーや自動アップデート処理が「何回実行されても安全であること(冪等性)」が絶対条件となる。すでにカラムが存在する場合にエラーで止まるのではなく、「何もしないで正常終了」させることで、多重実行のリスクを完全に排除している。
2. ポーリング(定期的再試行)と `DoEvents`
マルチユーザー環境において、他ユーザーがテーブルを開いている時間はごく一時的であることが多い。したがって、「エラーになったら即座に諦める」のではなく、「2秒待って、もう一度トライする」を5回繰り返す(計10秒の猶予)設計にすることで、運用の成功率を劇的に跳ね上げることができる。
また、待機中に `SleepWithDoEvents` を挟むことで、Accessの画面が「応答なし」になるのを防ぎ、バックグラウンドでの親切な挙動を実現している。
3. DAOオブジェクトの厳格なライフサイクル管理
VBAにおけるメモリリークは、データベースの破損やLDBファイル(ロックファイル)の残留を引き起こす元凶だ。
リトライループの各フェーズで `Set tdf = Nothing` や `Set fld = Nothing` を明示的に行い、ACEエンジンが保持している内部キャッシュやロック参照を確実に解放してから再試行を行っている。
—
結論:現場で信頼されるシステムを作るために
Access VBAは「手軽に作れる」がゆえに、こうしたマルチユーザー環境を無視した幼稚なコードが放置されがちだ。その結果、「時々謎のエラーで止まる使えないシステム」という悪評が立つ。
今回紹介した排他制御とリトライのロジックは、そのままあなたの開発資産となる。プロフェッショナルの誇りを持って、堅牢で止まらない業務システムを構築してほしい。
