【実務・中級編】【実務】テーブル定義の変更履歴を「監査ログテーブル」に自動記録するトリガー設計 – Access VBA解析バイブル

スポンサーリンク

Access開発の現場において、多くのエンジニアが陥る「死のトラップ」がある。それは、「テーブル定義は不変である」という根拠のない思い込みだ。

実務では、場当たり的なフィールド追加、データ型の安易な変更、あるいは意図しないインデックスの削除が、ある日突然システムを崩壊させる。そしてその時、犯人は常に「不明」だ。

世界最高峰の現場で求められるのは、単に動くコードではない。「いつ、誰が、何を、どう変えたのか」を沈黙の監視者として記録し続ける、堅牢な監査ログシステムである。今回は、Access VBAのDAO(Data Access Objects)を極限まで使いこなし、テーブル定義の変更を自動検知・記録するプロフェッショナルな設計思想を伝授する。

—

1. なぜ「標準機能」に頼ってはいけないのか

AccessにはSQL ServerのようなDDLトリガーは存在しない。プロパティウィンドウでフィールド名を変えれば、それは音もなく書き換わる。

初心者は「変更時にメモを残す」という運用ルールを徹底させようとするが、それは幻想だ。人間は必ず忘れる。だからこそ、「現在の定義」と「前回の定義(スナップショット)」を比較し、その差分(デルタ)を自動抽出するロジックが必要になる。

監査ログ設計の三原則

1. 非破壊性: 既存のテーブル構造に一切の影響を与えないこと。
2. 完全性: 名前、型、サイズ、属性(Required等)のすべての変化を逃さないこと。
3. 自律性: 開発者が意識せずとも、特定のイベント(アプリ起動時や管理者メニュー実行時)に実行されること。

—

2. 監査ログテーブルの設計

まずは、証跡を保存する「器」を定義する。このテーブル自体が変更されては元も子もないため、これはシステム専用の隠しテーブル、あるいはバックエンドDBに配置するのが定石だ。

| フィールド名 | データ型 | 説明 |
| :— | :— | :— |
| LogID | オートナンバー | 主キー |
| TargetTable | 短いテキスト | 変更されたテーブル名 |
| TargetField | 短いテキスト | 変更されたフィールド名(新規追加時はその名称) |
| ActionType | 短いテキスト | ADD / DELETE / MODIFY |
| PropertyName | 短いテキスト | Type, Size, Name 等の属性名 |
| OldValue | 長いテキスト | 変更前の値 |
| NewValue | 長いテキスト | 変更後の値 |
| ChangedBy | 短いテキスト | 実行ユーザー名 |
| ChangedAt | 日付/時刻 | 記録日時 |

—

3. 【実戦】テーブル定義・自動監視エンジン

以下のコードは、単なる「テーブル一覧の書き出し」ではない。現在のTableDefsコレクションをスキャンし、前回のスナップショットと比較して差分のみを抽出するアーキテクチャだ。

※ 簡略化のため、スナップショット用のワークテーブル `sys_SchemaSnapshot` が存在することを前提とする。

Option Compare Database
Option Explicit

‘
‘ Module: mod_SchemaAuditor
‘ Description: テーブル定義の変更を検知し、監査ログに記録する
‘ Author: Chief Architect
‘

Public Sub AuditTableDefinitions()
On Error GoTo Err_Handler

Dim db As DAO.Database
Dim tdf As DAO.TableDef
Dim fld As DAO.Field
Dim rsSnapshot As DAO.Recordset
Dim strSQL As String
Dim currentUser As String

Set db = CurrentDb
currentUser = CreateObject(“WScript.Network”).UserName ‘ OSユーザー名を取得

‘ 1. システムテーブルを除く全ユーザーテーブルを走査
For Each tdf In db.TableDefs
‘ MSysで始まるシステムテーブルや一時テーブルをスキップ
If Not (tdf.Name Like “MSys” Or tdf.Name Like “~”) Then

Debug.Print “Auditing Table: ” & tdf.Name

‘ 2. 各フィールドの属性をチェック
For Each fld In tdf.Fields
Call CompareAndLogField(tdf.Name, fld, currentUser)
Next fld
End If
Next tdf

‘ 3. 最後に現在の状態をスナップショットとして保存(次回比較用)
‘ ※実際の実務では、比較が終わった後に一括でSnapshotテーブルを更新するロジックを組む

MsgBox “スキーマ監査が完了しました。”, vbInformation

Exit_Handler:
Set db = Nothing
Exit Sub

Err_Handler:
Debug.Print “Error: ” & Err.Description
Resume Exit_Handler
End Sub

Private Sub CompareAndLogField(ByVal tblName As String, ByRef fld As DAO.Field, ByVal userName As String)
Dim db As DAO.Database
Dim rs As DAO.Recordset
Dim isNew As Boolean

Set db = CurrentDb

‘ スナップショットテーブルから該当フィールドの情報を取得
‘ sys_SchemaSnapshot: [TableName], [FieldName], [DataType], [Size]
Set rs = db.OpenRecordset( _
“SELECT FROM sys_SchemaSnapshot WHERE TableName='” & tblName & “‘ AND FieldName='” & fld.Name & “‘”, _
dbOpenSnapshot)

If rs.EOF Then
‘ 新規フィールドの検知
Call WriteAuditLog(tblName, fld.Name, “ADD”, “Field”, “”, fld.Name, userName)
‘ スナップショットへの追加ロジック(中略)
Else
‘ 既存フィールドの定義変更チェック
‘ 型(Type)の変更チェック
If rs!DataType <> fld.Type Then
Call WriteAuditLog(tblName, fld.Name, “MODIFY”, “DataType”, rs!DataType, fld.Type, userName)
End If

‘ サイズ(Size)の変更チェック
If rs!Size <> fld.Size Then
Call WriteAuditLog(tblName, fld.Name, “MODIFY”, “Size”, rs!Size, fld.Size, userName)
End If
End If

rs.Close
Set rs = Nothing
End Sub

Private Sub WriteAuditLog(tbl As String, fld As String, action As String, prop As String, oldVal As String, newVal As String, usr As String)
Dim sql As String
‘ インジェクション対策としてReplaceを噛ませるのがプロの作法
sql = “INSERT INTO sys_AuditLog (TargetTable, TargetField, ActionType, PropertyName, OldValue, NewValue, ChangedBy, ChangedAt) ” & _
“VALUES (‘” & tbl & “‘, ‘” & fld & “‘, ‘” & action & “‘, ‘” & prop & “‘, ‘” & _
Replace(oldVal, “‘”, “””) & “‘, ‘” & Replace(newVal, “‘”, “””) & “‘, ‘” & usr & “‘, Now())”

CurrentDb.Execute sql, dbFailOnError
End Sub

—

4. 現場で差がつく「極限の知見」

① `DAO.TableDef` のライフサイクルを意識せよ

`TableDefs` コレクションをループしている最中に、そのテーブル構造を変更するような操作(`Refresh` や `Append`)を行うと、列挙インデックスが狂い、実行時エラーが発生する。比較ロジックと更新ロジックは明確に分離せよ。

② データ型の数値(Enum)を「翻訳」して保存せよ

`fld.Type` は `dbLong` なら `4`、`dbText` なら `10` という数値を返す。ログに「4から10に変わった」と記録されても、後で人間が読めない。`Select Case` 文を用いて、”Long Integer”, “Text” といった文字列に変換して保存するヘルパー関数を用意するのが、保守性の高いコードの証だ。

③ インデックスとリレーションシップの罠

フィールド名や型だけでなく、`tdf.Indexes` コレクションも同様に監視対象に含めるべきだ。主キーが外されたり、重複許可設定が勝手に変えられるトラブルは、フィールド変更よりも発見が遅れ、被害が甚大になる。

④ パフォーマンスへの配慮

テーブル数やフィールド数が多い場合、毎回全スキャンを行うとアプリの起動を阻害する。

  • 開発環境では「起動時」に実行。
  • 本番環境では「管理者ツール実行時」や「週次バッチ」として実行。

このように、実行フェーズを切り分ける設計にせよ。

—

5. 終わりに

「誰が変えたかわからない」という言葉は、エンジニアの敗北宣言に等しい。
今回紹介したスキーマ監査の実装は、システムに対する「統制」そのものである。コード一つで、データベースは単なるデータの箱から、自らの変化を語る知的なプラットフォームへと昇華する。

このロジックを自身のライブラリに組み込み、二度と「見えない変更」に怯えることのない堅牢なシステムを構築してほしい。それが、プロフェッショナルとしての矜持だ。

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