【上級】Access VBAを掌握する極限の知見:テーブル定義変更の監査ログ自動記録アーキテクチャ
レガシーシステムの最前線に立ち続ける我々にとって、Accessは単なる簡易データベースではない。適切なガバナンスとアーキテクチャを与えれば、堅牢な基幹クライアントとして機能する。しかし、長年運用されるAccessシステムにおいて、最も恐ろしいリスクは何か?
それは、「誰が、いつ、どのテーブルの構造(DDL)を勝手に変更したか分からない」というカオス状態だ。
Access(DAO/Jet Engine)は、SQL Serverのような強力なサーバーサイドDDLトリガーを持たない。UIからの操作、あるいは無知な開発者によるVBAからの動的変更により、テーブル定義は密かに破壊され、依存するクエリやフォームが連鎖的に崩壊する。
この泥沼に終止符を打つため、今回はDAOのイベントフック、Windows API、そして厳密なメモリ管理を融合させた「テーブル定義自動監査システム」の設計思想と実装コードを公開する。
—
1. アーキテクチャの全体像:なぜDAOイベントだけでは不十分か
Accessにおけるテーブル定義の変更を検知するには、DAOの `Container` や `Document` オブジェクト、あるいは `TableDef` のコレクション変更を監視する必要がある。しかし、純粋なVBAコードだけでこれを完璧に捕捉するのは困難だ。
真に堅牢な監査システムを構築するためには、以下の3要素を統合しなければならない。
1. セッション情報の特定(誰が): Accessの組み込み関数だけでなく、Windows APIによるOSレベルのユーザー名・マシン名の取得。
2. 変更前後のスナップショット(何を): TableDefのコレクション、フィールドの型・サイズ、インデックス情報の差分抽出。
3. トランザクションとエラーバウンダリ(確実に残す): 監査ログ書き込み自体の失敗が、本来の処理をブロックしないための堅牢な例外処理。
—
2. 実装:極限まで最適化された監査ログ・エンジン
以下に、実務で使用可能なプロダクション品質のコードを示す。
このコードは、オブジェクトの明示的解放(メモリリークの完全排除)、APIによるセッション情報の取得、そして変更検知ロジックをカプセル化している。
Option Compare Database
Option Explicit
‘ ==============================================================================
‘ Windows API Declarations for Session Context
‘ ==============================================================================
If VBA7 Then
Private Declare PtrSafe Function GetUserName Lib “advapi32.dll” Alias “GetUserNameA” (ByVal lpBuffer As String, nSize As Long) As Long
Private Declare PtrSafe Function GetComputerName Lib “kernel32” Alias “GetComputerNameA” (ByVal lpBuffer As String, nSize As Long) As Long
Else
Private Enum LongPtr
[_]
End Enum
Private Declare Function GetUserName Lib “advapi32.dll” Alias “GetUserNameA” (ByVal lpBuffer As String, nSize As Long) As Long
Private Declare Function GetComputerName Lib “kernel32” Alias “GetComputerNameA” (ByVal lpBuffer As String, nSize As Long) As Long
End If
‘ ==============================================================================
‘ 定数定義
‘ ==============================================================================
Private Const AUDIT_TABLE_NAME As String = “Sys_TableDefinition_AuditLog”
‘ ==============================================================================
‘ ユーティリティ: 現在のWindowsユーザー名を取得
‘ ==============================================================================
Private Function GetCurrentWindowsUser() As String
Dim buffer As String
Dim size As Long
Dim success As Long
buffer = String(255, 0)
size = 255
success = GetUserName(buffer, size)
If success <> 0 Then
GetCurrentWindowsUser = Left(buffer, Instr(buffer, Chr(0)) – 1)
Else
GetCurrentWindowsUser = “Unknown_User”
End If
End Function
‘ ==============================================================================
‘ ユーティリティ: 現在のコンピュータ名を取得
‘ ==============================================================================
Private Function GetCurrentMachineName() As String
Dim buffer As String
Dim size As Long
Dim success As Long
buffer = String(255, 0)
size = 255
success = GetComputerName(buffer, size)
If success <> 0 Then
GetCurrentMachineName = Left(buffer, Instr(buffer, Chr(0)) – 1)
Else
GetCurrentMachineName = “Unknown_Machine”
End If
End Function
‘ ==============================================================================
‘ コア機能: テーブル定義変更の監査ログ記録
‘ ==============================================================================
Public Sub LogTableDefinitionChange(ByVal TableName As String, ByVal ActionType As String, ByVal Details As String)
Dim db As DAO.Database
Dim rs As DAO.Recordset
‘ エラーハンドリングの境界設定(監査ログの失敗でメイン処理を落とさない)
On Error GoTo ErrorHandler
Set db = CurrentDb
‘ 監査ログテーブルが存在しない場合は動的生成(セルフヒーリング機構)
Call EnsureAuditTableExists(db)
Set rs = db.OpenRecordset(AUDIT_TABLE_NAME, dbOpenDynaset, dbAppendOnly)
With rs
.AddNew
!LogTimestamp = Now()
!WindowsUser = GetCurrentWindowsUser()
!MachineName = GetCurrentMachineName()
!AccessUser = CurrentUser()
!TargetTableName = TableName
!ActionType = ActionType ‘ ADD, DROP, ALTER など
!ChangeDetails = Details ‘ 変更されたフィールドやプロパティの詳細
.Update
End With
CleanUp:
‘ 【極限の知見】DAOオブジェクトの明示的解放によるメモリ肥大化の防止
If Not rs Is Nothing Then
rs.Close
Set rs = Nothing
End If
Set db = Nothing
Exit Sub
ErrorHandler:
‘ ログ記録の失敗はイミディエイトウィンドウに出力するに留め、システム停止を防ぐ
Debug.Print “[CRITICAL] Failed to write audit log: ” & Err.Description
Resume CleanUp
End Sub
‘ ==============================================================================
‘ セルフヒーリング: 監査テーブルの動的プロビジョニング
‘ ==============================================================================
Private Sub EnsureAuditTableExists(ByRef db As DAO.Database)
Dim tdf As DAO.TableDef
Dim exists As Boolean
exists = False
For Each tdf In db.TableDefs
If tdf.Name = AUDIT_TABLE_NAME Then
exists = True
Exit For
End If
Next tdf
If Not exists Then
Set tdf = db.CreateTableDef(AUDIT_TABLE_NAME)
With tdf
.Append .CreateField(“LogID”, dbLong)
.Fields(“LogID”).Attributes = .Fields(“LogID”).Attributes + dbAutoIncrField
.Append .CreateField(“LogTimestamp”, dbDate)
.Append .CreateField(“WindowsUser”, dbText, 50)
.Append .CreateField(“MachineName”, dbText, 50)
.Append .CreateField(“AccessUser”, dbText, 50)
.Append .CreateField(“TargetTableName”, dbText, 100)
.Append .CreateField(“ActionType”, dbText, 20)
.Append .CreateField(“ChangeDetails”, dbMemo)
End With
db.TableDefs.Append tdf
‘ 主キーの設定
db.Execute “ALTER TABLE ” & AUDIT_TABLE_NAME & ” ADD CONSTRAINT PK_AuditLog PRIMARY KEY (LogID);”, dbFailOnError
End If
CleanUp:
Set tdf = Nothing
End Sub
—
3. チーフアーキテクトが解説する実装の急所
上記のコード片には、長年のトラブルシューティングから導き出された「エンジニアの美学と防御策」が組み込まれている。
① オブジェクトのライフサイクル管理とメモリリーク対策
VBAの `Dim rs As DAO.Recordset` や `Set db = CurrentDb` は、スコープを抜けても即座にCOMコンポーネントの参照カウントがゼロになるとは限らない。特にAccessのマルチユーザ環境や長期間稼働するプロセスでは、これがメモリリークや「Jetコンテキストの肥大化」を引き起こす。
コード内にあるように、`CleanUp` ラベルを必ず用意し、`rs.Close` の後に `Set rs = Nothing` を明示的に実行する習慣を徹底すること。
② 監査ログ起因のクラッシュを防ぐ「例外の隔離(Error Boundary)」
監査システム自体がバグを起こしたり、ネットワーク切断等で監査テーブルに書き込めなくなったりしたせいで、本来の業務トランザクション全体がロールバックされては本末転倒である。
`LogTableDefinitionChange` 内では `On Error GoTo ErrorHandler` を厳格に配置し、万が一の書き込み失敗時は `Debug.Print` でコンソールに流すに留め、業務処理を続行させる設計にしている。これはエンタープライズ・アーキテクチャの基本原則である。
③ セルフヒーリング(自動プロビジョニング)
システムを別環境に移行した際、「監査ログテーブルを作り忘れてエラー落ちした」というインフラ担当者のミスを誘発してはならない。`EnsureAuditTableExists` サブルーチンにより、初回実行時に自動的に物理テーブルと主キー制約を構築する仕組みを内蔵している。
—
4. 運用上の極意:DDL変更のフックをどう組むか
この監査エンジンを実稼働させるには、「いつ `LogTableDefinitionChange` を呼ぶか」というトリガーの問題が残る。
AccessのフォームやVBAからテーブル構造を変更する場合(例:DAOを使った動的なフィールド追加など)、必ず以下のようなラッパー関数経由で実行するコーディング規約をチームに課すこと。
‘ 開発者が直接 TableDefs.Append を書くのではなく、このラッパーを通す規約にする
Public Sub SafeAddTableField(ByVal TargetTableName As String, ByVal FieldName As String, ByVal FieldType As Integer, Optional ByVal FieldSize As Integer = 0)
Dim db As DAO.Database
Dim tdf As DAO.TableDef
Dim fld As DAO.Field
Set db = CurrentDb
Set tdf = db.TableDefs(TargetTableName)
If FieldSize > 0 Then
Set fld = tdf.CreateField(FieldName, FieldType, FieldSize)
Else
Set fld = tdf.CreateField(FieldName, FieldType)
End If
tdf.Fields.Append fld
‘ ★ここで監査ログを自動記録
Call LogTableDefinitionChange(TargetTableName, “ADD_FIELD”, “Added field: ” & FieldName & ” (Type: ” & FieldType & “)”)
Set fld = Nothing
Set tdf = Nothing
Set db = Nothing
End Sub
—
結びにかえて
Access VBAを「おもちゃのスクリプト言語」と侮る者は、システムの複雑化に飲み込まれて自滅する。
メモリのライフサイクルを支配し、OSのAPIを直接叩き、堅牢なエラーバウンダリを引くこと。このレベルのエンジニアリングを施せば、Accessはレガシーの呪縛を断ち切り、予測可能で信頼性の高いインフラへと昇華する。
あなたのシステムに真のガバナンスを。今日からこのアーキテクチャを導入し、コードベースをコントロール下に置くのだ。
