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

スポンサーリンク

こんにちは!日々の業務効率化、本当にお疲れ様です。

Accessを使っていると、「誰が、いつ、どのテーブルの、どのフィールド定義を変更したのか」を把握したくなる瞬間はありませんか?
複数人でデータベースを共有して開発しているときや、本番運用のシステムで「いつの間にかフィールドの型が変わっていてエラーになった……」というトラブルを防ぐためにも、テーブル定義(スキーマ)の変更履歴を自動で記録する仕組み(監査ログ)は、プロフェッショナルな開発において必須の装備です。

実は、Access(Jet/ACEエンジン)には、SQL Serverなどの大型RDBMSにあるような「DDLトリガー(テーブル定義変更を自動検知するトリガー)」が標準で備わっていません。

「じゃあ、Accessでは自動記録は無理なの?」

いいえ、そんなことはありません!
AccessにはDAO(Data Access Objects)という、データベースの構造そのものをプログラムで自由自在に操れる強力な仕組みがあります。このDAOの特性を理解し、テーブル定義を変更する処理をVBAでラップ(包み込む)してあげることで、完璧な「監査ログ自動記録トリガー」を自作することができるのです。

この記事では、マクロの記録を卒業し、Access VBAの真の力を引き出したいあなたに向けて、オブジェクトの寿命(ライフサイクル)やメモリの管理といった一歩進んだプロの視点も交えながら、優しく、かつ深く解説していきます。

ここをクリアすれば、Access VBAの基本だけでなく、データベース設計の本質までバッチリ掴めますよ。それでは、一緒に学んでいきましょう!

—

1. 全体像:トリガー設計の仕組み

今回の設計は、以下の図のようなアプローチをとります。

[開発者・システム]
│
▼ (直接テーブルを触るのではなく…)
┌──────────────────────────────────────────────┐
│ VBA変更ラッパー関数 (SafeModifyField) │
├──────────────────────────────────────────────┤
│ 1. DAOで対象テーブルのフィールドを変更・追加 │
│ 2. 変更が成功したら、監査ログテーブルに書き込み │
└──────────────────────────────────────────────┘
│ │
▼ (構造変更) ▼ (ログ記録)
┌──────────────────────┐ ┌──────────────────────┐
│ ターゲットテーブル │ │ 監査ログテーブル │
│ (tbl_Users など) │ │ (tbl_SchemaLog) │
└──────────────────────┘ └──────────────────────┘

直接テーブルのデザインビューで変更するのではなく、「VBAの関数を通して安全にスキーマを変更し、同時にその履歴をログテーブルに刻む」というアプローチです。
これを行うことで、変更の失敗時のロールバック(元に戻す処理)や、エラーハンドリング、そして「誰がいつ何をしたか」の記録を完全に1つの処理にまとめることができます。

—

2. ログを保存する「監査ログテーブル」を作ろう

まずは、変更履歴を保存するためのテーブル`tbl_SchemaLog`を作成しましょう。
以下の定義でテーブルを新規作成してください。

監査ログテーブル (`tbl_SchemaLog`) の構成

| フィールド名 | データ型 | 説明 |
| :— | :— | :— |
| LogID | オートナンバー型 | 主キー |
| LogDateTime | 日付/時刻型 | 変更が実行された日時(既定値:`Now()`) |
| OperatorName | 短いテキスト型 | 実行したユーザー(PCのログイン名など) |
| TableName | 短いテキスト型 | 変更対象のテーブル名 |
| FieldName | 短いテキスト型 | 変更対象のフィールド名 |
| ActionType | 短いテキスト型 | 変更内容(”ADD”:追加、”DELETE”:削除、”MODIFY”:型変更 など) |
| DataTypeInfo | 短いテキスト型 | 変更後のデータ型や設定値 |
| Description | 長いテキスト型 | 変更の理由や詳細メモ |

—

3. 知っておきたい!DAOの「ライフサイクル」と「CurrentDb」の真実

コードを書く前に、先輩としてどうしても伝えておきたい「Access VBAの最重要ルール」があります。
それは、`CurrentDb` の扱い方です。

よくネットのサンプルコードで、以下のような書き方を見かけませんか?

‘ 実はあまり良くない書き方の例
CurrentDb.TableDefs(“tbl_Users”).Fields.Append …

実は、`CurrentDb` という関数は、呼び出されるたびに「現在のデータベースの新しいインスタンス(オブジェクト)」をメモリ上に生成し直すという非常に重い処理を行っています。
そのため、上記のように1行で完結させてしまうと、処理が終わった瞬間にオブジェクトが破棄され、メモリリークの原因になったり、動作が極端に遅くなったりします。

これを防ぐためのプロの鉄則がこれです。

> 「`CurrentDb` は必ず一度変数(`DAO.Database`)に代入し、使い終わったら明視的に `Nothing` で解放する」

このオブジェクトの「ライフサイクル(寿命)」をコントロールすることこそが、Accessをクラッシュさせずに安定して動かす最大の秘訣です。

—

4. 実装:監査ログ付きテーブル定義変更コード

それでは、標準モジュールを作成し、以下のVBAコードを貼り付けてみましょう。
このコードは、「新しいフィールドを安全に追加し、同時に監査ログへ自動記録する」という実務直結の関数です。

Option Compare Database
Option Explicit

‘ Windows APIを使って、PCにログインしているユーザー名を取得します
If VBA7 Then
Private Declare PtrSafe Function GetUserName Lib “advapi32.dll” Alias “GetUserNameA” ( _
ByVal lpBuffer As String, _
ByRef nSize As Long) As Long
Else
Private Declare Function GetUserName Lib “advapi32.dll” Alias “GetUserNameA” ( _
ByVal lpBuffer As String, _
ByRef nSize As Long) As Long
End If

”’

”’ 現在のWindowsログインユーザー名を取得する関数
”’

Public Function GetClientUser() As String
Dim buffer As String 25
Dim ret As Long
ret = GetUserName(buffer, 25)
If ret <> 0 Then
GetClientUser = Left(buffer, InStr(buffer, vbNullChar) – 1)
Else
GetClientUser = “Unknown”
End If
End Function

”’

”’ 安全にフィールドを追加し、その内容を監査ログに自動記録します
”’

”’ 対象のテーブル名 ”’ 追加するフィールド名 ”’ データ型 (例: dbText, dbLong, dbDate など) ”’ テキスト型の場合の文字数(省略可) ”’ 監査ログに残すメモ Public Sub AddFieldWithLog( _
ByVal targetTable As String, _
ByVal newFieldName As String, _
ByVal dataType As Long, _
Optional ByVal fieldSize As Integer = 0, _
Optional ByVal memo As String = “”)

Dim db As DAO.Database
Dim tdf As DAO.TableDef
Dim fld As DAO.Field
Dim logRs As DAO.Recordset
Dim isSuccess As Boolean

‘ エラートラップの設定(予期せぬエラーが起きても安全に終了させるため)
On Error GoTo Err_Handler

‘ 1. Databaseオブジェクトを「1回だけ」取得する(超重要!)
Set db = CurrentDb

‘ 2. 対象テーブルの存在チェック
On Error Resume Next
Set tdf = db.TableDefs(targetTable)
On Error GoTo Err_Handler

If tdf Is Nothing Then
Err.Raise vbObjectError + 1001, “AddFieldWithLog”, “対象のテーブル ‘” & targetTable & “‘ が見つかりません。”
End If

‘ 3. 同名のフィールドがすでに存在しないかチェック
On Error Resume Next
Set fld = tdf.Fields(newFieldName)
On Error GoTo Err_Handler

If Not fld Is Nothing Then
Err.Raise vbObjectError + 1002, “AddFieldWithLog”, “フィールド ‘” & newFieldName & “‘ は既に存在します。”
End If

‘ 4. フィールドオブジェクトの作成と追加
‘ ※テキスト型(dbText)の場合のみ、サイズを指定します
If dataType = dbText And fieldSize > 0 Then
Set fld = tdf.CreateField(newFieldName, dataType, fieldSize)
Else
Set fld = tdf.CreateField(newFieldName, dataType)
End If

‘ ここで実際にテーブル定義にフィールドを追加(確定)します
tdf.Fields.Append fld

‘ テーブル定義情報を最新に更新
db.TableDefs.Refresh

‘ フィールド追加が成功したフラグを立てる
isSuccess = True

‘ 5. 監査ログテーブル(tbl_SchemaLog)への自動書き込み
Set logRs = db.OpenRecordset(“tbl_SchemaLog”, dbOpenDynaset)
With logRs
.AddNew
!LogDateTime = Now()
!OperatorName = GetClientUser()
!TableName = targetTable
!FieldName = newFieldName
!ActionType = “ADD_FIELD”
!DataTypeInfo = “Type: ” & dataType & ” (Size: ” & fieldSize & “)”
!Description = memo
.Update
End With

MsgBox “フィールド ‘” & newFieldName & “‘ を追加し、監査ログに記録しました!”, vbInformation, “成功”

Exit_Handler:
‘ 6. 後片付け(メモリリークを防ぐため、作成したオブジェクトを逆順で確実に解放する)
On Error Resume Next
If Not logRs Is Nothing Then logRs.Close: Set logRs = Nothing
Set fld = Nothing
Set tdf = Nothing
If Not db Is Nothing Then db.Close: Set db = Nothing
Exit Sub

Err_Handler:
‘ エラーが発生した場合は、メッセージを表示して安全に終了する
MsgBox “エラーが発生しました。” & vbCrLf & _
“エラー番号: ” & Err.Number & vbCrLf & _
“エラー内容: ” & Err.Description, vbCritical, “エラー終了”
Resume Exit_Handler
End Sub

—

5. コードの意味と「陥りやすい罠」の解説

このコードには、Accessを安全に制御するための技術がぎゅっと詰まっています。ポイントを絞って解説しますね。

① Windows APIによる「真の実行ユーザー」の特定

Accessの `CurrentUser` 関数を使うと、デフォルトではすべて「Admin」という名前が返ってきてしまい、誰が操作したか分かりません。
そこで、Windowsから直接PCのログインユーザー名を取得するAPI(`GetUserName`)を使用しています。これで「誰が変更したか」を正確にログに刻めます。

② 二重定義エラーの防止(エラー番号 3191 の回避)

すでに存在するフィールドをもう一度追加しようとすると、Accessは「3191: フィールドは既に定義されています」というエラーを吐いて強制終了してしまいます。
コード内の `3. 同名のフィールドがすでに存在しないかチェック` の部分で、事前に追加可能かどうかを優しくチェックしているため、システムがクラッシュすることはありません。

③ オブジェクトの徹底解放(ライフサイクルのクローズ)

VBAでは、開いたオブジェクトをそのままにしておくと、Accessのメモリ空間が圧迫され、最悪の場合「リソース不足です」というエラーでデータベースが壊れる原因になります。
`Exit_Handler` の中で、`Set db = Nothing` のように、使用したオブジェクトを綺麗に片付けているのが、プロのコードの証です。

—

6. 動かしてみよう!テスト用コード

作成した関数が正しく動くか、イミディエイトウィンドウやテスト用マクロから呼び出してみましょう。

例えば、既存のテーブル `tbl_Users`(事前に作成しておいてください)に、新しく「郵便番号(ZipCode)」というテキスト型のフィールドを追加する場合は、以下のように呼び出します。

Sub Test_AddField()
‘ tbl_Users テーブルに、長さ8桁の「ZipCode」フィールドを追加し、ログに記録します
Call AddFieldWithLog( _
targetTable:=”tbl_Users”, _
newFieldName:=”ZipCode”, _
dataType:=dbText, _
fieldSize:=8, _
memo:=”顧客管理機能の強化に伴う、郵便番号フィールドの追加” _
)
End Sub

実行後、`tbl_Users` をデザインビューで開くと「ZipCode」フィールドが追加されており、同時に `tbl_SchemaLog` を開くと、以下のようにバッチリ変更履歴が記録されているはずです!

| LogID | LogDateTime | OperatorName | TableName | FieldName | ActionType | DataTypeInfo | Description |
| :— | :— | :— | :— | :— | :— | :— | :— |
| 1 | 2023/10/27 10:00:15 | taro.yamada | tbl_Users | ZipCode | ADD_FIELD | Type: 10 (Size: 8) | 顧客管理機能の強化に伴う、郵便番号フィールドの追加 |

—

まとめ:ここをクリアすれば、Access VBAの基本はバッチリです!

今回は、DAOを使った「テーブル定義の制御」と「監査ログへの自動記録」の実装方法を学びました。

一見難しそうに見える「スキーマの変更」も、
1. `CurrentDb` を変数に格納してライフサイクルを管理する
2. 事前に存在チェックをしてエラーを未然に防ぐ
3. 処理の成功と同期してログテーブルに書き込む

というステップを一つずつ踏めば、とても安全に、かつスマートに実現できることがお分かりいただけたかと思います。

これがスムーズに書けるようになれば、マクロの記録レベルからは完全に脱却し、「Accessのシステム全体をコードで支配・コントロールできている状態」と言っても過言ではありません。

実務の現場で「このシステム、誰がテーブル変えたの?」という不毛な犯人探しに疲れたときは、ぜひこの監査ログトリガーを組み込んでみてください。あなたの評価もうなぎ登り間違いなしですよ!

何か分からないことがあれば、いつでも頼ってくださいね。これからも一緒に、一歩ずつ進んでいきましょう!

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