【プロが教えるAccess VBA】マルチユーザー環境でのテーブル定義変更:排他制御の極意
こんにちは。大規模なAccessデータベースシステムの設計から改修まで、数々の修羅場をくぐり抜けてきたチーフアーキテクトの私だ。
開発中のローカル環境では軽快に動いていたVBAコードが、いざ本番のマルチユーザー環境(共有ファイルサーバー上)にデプロイされた途端、「実行時エラー ‘3211’: データベースを排他的にオープンできません」 や 「他のユーザーまたはプロセスが使用しています」 という冷酷なエラーメッセージを吐き出して停止する……。
君も、この悪夢のような現象に頭を抱えた経験はないだろうか?
素人がやりがちな「とりあえずエラーを無視する(`On Error Resume Next`)」ような場当たり的なコードは、データベースを物理的に破壊し、全社を巻き込む大惨事を引き起こす。今回は、マルチユーザー環境において安全かつ確実にテーブル定義を変更(ALTER/CREATE等)するための「排他ロック管理」の極意を、ロジカルかつシャープに伝授しよう。
—
なぜ「テーブル定義の動的変更」はマルチユーザー環境で破綻するのか?
まず、Access(ACE/Jetエンジン)のアーキテクチャの本質を理解しなければならない。
VBAから `TableDef` オブジェクトや `CurrentDb.Execute` を用いてDDL(Data Definition Language)を実行する場合、Accessエンジンは対象のテーブルに対して「排他ロック(Exclusive Lock)」を要求する。
しかし、社内ネットワーク上の共有フォルダに置かれたバックエンド(またはモノリシックな単一ファイル)に他のユーザーが接続している瞬間、そのテーブルは他者によって「共有ロック(Shared Lock)」または「読み取りロック」で占有されている。
ここで発生するのが、デッドロックおよびリソース競合だ。
未熟な設計のプログラムは、他ユーザーの接続を無視して無理やり変更を試みるか、あるいはロック解放のタイミングを制御できずにタイムアウトを迎える。これが「バグの温床」と呼ばれる所以である。
プロのエンジニアが守るべき鉄則はただ一つ。
「変更対象のデータベースを完全に孤立させ、接続ユーザーのセッションを安全に排除(または待機)してから処理を実行する」ことだ。
—
堅牢な排他制御を実現する3つのステップ
マルチユーザー環境でテーブル定義を安全に変更するためには、以下の3つの防壁をコードに実装する必要がある。
1. バックエンド(データ側)の分離設計
フロントエンド(UI/VBA)とバックエンド(Table)が同一ファイルにあるモノリシックな構造のまま、コードでテーブル定義をいじってはならない。構造変更は常に「バックエンドファイルに対する排他接続」として行うべきだ。
2. 明示的な `OpenDatabase` による排他オープン
`CurrentDb` はカレントセッションの共有コンテキストを返すため、テーブル定義の動的変更には不向きである。DAOの `OpenDatabase` メソッドを `exclusive:=True` で明示的に呼び出し、OSレベルでの排他制御権を獲得する。
3. リトライ&フォールバック機構(エラーハンドリング)
他ユーザーが接続している確率を考慮し、即死させるのではなく、一定時間待機(ポーリング)するか、あるいはユーザーに優しく切断を促すロジックを挟む。
—
【コピペOK】プロダクション品質のテーブル定義変更モジュール
それでは、実務の現場でそのまま使える、極めて堅牢なVBAコードを公開しよう。
このコードは、指定したバックエンドデータベースを排他モードで開けなければ安全に処理を中断し、開くことができれば安全にテーブル定義(今回はフィールドの追加)を行うサンプルだ。
Option Compare Database
Option Explicit
‘ =========================================================================
‘ プロジェクト名: 堅牢なテーブル定義変更モジュール
‘ 概要 : マルチユーザー環境で安全に排他ロックを取得し、テーブル定義を変更する
‘ 著者 : チーフアーキテクト
‘ =========================================================================
Public Sub SafeModifyTableDefinition()
Dim dbBackend As DAO.Database
Dim tdf As DAO.TableDef
Dim fld As DAO.Field
Dim strDbPath As String
Dim lngRetryCount As Long
Dim maxRetries As Long
Dim isLocked As Boolean
‘ — 設定値 —
‘ ※実務では環境に合わせてパスを変更、またはフロントエンドから動的取得すること
strDbPath = “C:\Database\Backend_Data.accdb” ‘ バックエンドのパス
maxRetries = 3 ‘ 最大リトライ回数
lngRetryCount = 0
isLocked = False
‘ 0. 事前存在確認
If Dir(strDbPath) = “” Then
MsgBox “指定されたデータベースが見つかりません: ” & strDbPath, vbCritical, “致命的エラー”
Exit Sub
End If
‘ =====================================================================
‘ Step 1: 排他ロック獲得トライアル(リトライ制御付き)
‘ =====================================================================
Do While lngRetryCount < maxRetries
On Error GoTo LockErrorHandler
' 【核心】OpenDatabaseの第3引数(Options)に True を指定し、第4引数で Exclusive:=True を強制
' これにより、他ユーザーが接続中の場合は即座にトラップエラー(Error 3045等)が発生する
Set dbBackend = DBEngine.OpenDatabase(strDbPath, False, True, ";PWD=YourPasswordIfNeeded")
' 排他オープンに成功した場合
isLocked = True
Exit Do
LockErrorHandler:
' エラー 3045: 使用中のためロックできません / エラー 3211: 排他オープンできません
If Err.Number = 3045 Or Err.Number = 3211 Then
lngRetryCount = lngRetryCount + 1
On Error GoTo 0
If lngRetryCount >= maxRetries Then
MsgBox “現在、他のユーザーがシステムを使用しているため、テーブル定義を変更できません。” & vbCrLf & _
“しばらく時間を置いてから再度実行してください。”, vbExclamation, “排他制御タイムアウト”
Exit Sub
End If
‘ 3秒待機してリトライ(ポーリング)
DoEvents
Application.Wait (Now + TimeValue(“00:00:03”))
Else
‘ 予期せぬエラー
Dim errNum As Long, errDesc As String
errNum = Err.Number
errDesc = Err.Description
On Error GoTo 0
MsgBox “予期せぬエラーが発生しました (” & errNum & “): ” & errDesc, vbCritical, “システムエラー”
Exit Sub
End If
Loop
If Not isLocked Then Exit Sub
‘ =====================================================================
‘ Step 2: 安全なトランザクション内でのテーブル定義変更
‘ =====================================================================
On Error GoTo TransErrorHandler
‘ トランザクション開始(データベース構造変更にはトランザクションが効かない場合もあるが、
vbaのロジック保護としてエラー時のロールバック前提の構造にする)
Set tdf = dbBackend.TableDefs(“M_Employees”)
‘ すでにフィールドが存在するかチェック(冪等性の担保)
If Not FieldExists(tdf, “LastModifiedDate”) Then
Set fld = tdf.CreateField(“LastModifiedDate”, dbDate)
tdf.Fields.Append fld
Debug.Print “フィールド ‘LastModifiedDate’ の追加に成功しました。”
Else
Debug.Print “フィールドは既に存在するため、追加をスキップしました。”
End If
‘ 変更を確実に永続化
dbBackend.Close
Set dbBackend = Nothing
MsgBox “テーブル定義の更新が正常に完了しました。”, vbInformation, “完了”
Exit Sub
TransErrorHandler:
Dim transErr As String
transErr = Err.Description
On Error Resume Next
If Not dbBackend Is Nothing Then
dbBackend.Close
Set dbBackend = Nothing
End If
MsgBox “テーブル定義の変更中にエラーが発生しました。” & vbCrLf & transErr, vbCritical, “ロールバック”
End Sub
‘ =========================================================================
‘ 補助関数: フィールドの存在チェック(冪等性の確保)
‘ =========================================================================
Private Function FieldExists(tdf As DAO.TableDef, fieldName As String) As Boolean
Dim fld As DAO.Field
FieldExists = False
For Each fld In tdf.Fields
If StrComp(fld.Name, fieldName, vbTextCompare) = 0 Then
FieldExists = True
Exit For
End If
Next fld
End Function
—
プロジェクトリーダーからの設計上のアドバイス
上記のコードを見て、「なぜここまで厳格に書く必要があるのか?」と思った読者へ、プロとしての重要な視点をいくつか補足しよう。
1. 冪等性(Idempotency)の確保
上記の `FieldExists` 関数を用いたチェックに注目してほしい。
システム開発の現場では、「同じスクリプトが2回実行されても安全であること(冪等性)」がプログラミングの鉄則だ。もしネットワークの瞬断などでスクリプトが途中で中断し、再度実行された際、すでに存在するフィールドを追加しようとして「エラー3191: フィールドは既に存在します」でクラッシュするような脆弱なコードは絶対に書いてはならない。
2. ユーザーインターフェース(UI)の切り離し
本番環境でテーブル定義を変更するようなマスターメンテナンスツールやバージョンアップバッチは、必ず「全ユーザーがログアウトしている状態(深夜帯やメンテナンスタイム)」に実行されることを前提に設計しつつ、それでもなお発生する偶発的な接続(ゴーストセッション等)に対して上記のようなコードで「防衛」を張るべきだ。
組織を守る堅牢なコードを書け
Accessは手軽に作れる反面、マルチユーザー環境における排他制御を誤ると、一瞬でデータベースが破損する諸刃の剣である。
「動けばいいや」というアマチュアの思考を捨て、こうしたプロフェッショナルな排他ロック管理を徹底することで、あなたの作るツールは「止まらない、信頼されるシステム」へと生まれ変わる。
次の開発からは、ぜひこの設計思想を取り入れてみてほしい。健闘を祈る。
