【プロ】Access VBAによる「テーブル定義のユニットテスト」:構造の整合性を自動検証する
長年、数多のレガシーシステムと向き合ってきた私にとって、Access VBAにおける最大の悪夢は「暗黙のスキーマ変更」である。
「フォーム側でデータ型エラーが発生する」「外部連携のインポートクエリが突然沈黙する」。その原因を辿ると、大抵は誰かが良かれと思って、あるいは不注意で、テーブル定義のフィールド長を変えた、あるいはインデックスを吹き飛ばした、という人災に行き着く。
Accessはプロトタイピングには神のようなスピードをもたらすが、本番運用における「構造の変更管理」に対しては、あまりにも無防備だ。GUIでいじれてしまうがゆえに、誰が・いつ・何を変えたのかがブラックボックス化しやすい。
この泥沼から抜け出す唯一の方法は、「コードによるテーブル定義の自動検証(ユニットテスト)」をデプロイメントの必須プロセスに組み込むことだ。今回は、DAO(Data Access Objects)を極限までチューニングし、メモリリークの罠を回避しながら、テーブル構造の整合性を1ミリの狂いもなく担保する極限のテストアーキテクチャを伝授する。
—
1. なぜAccessに「テーブル定義のテスト」が必要なのか
多くの開発者は、VBAで「データ」のバリデーションは行うが、「スキーマ(構造)」のバリデーションは怠る。
しかし、システム間連携や複合クエリが絡むエンタープライズ領域において、スキーマの不整合は致命傷となる。
手動での目視確認は、疲弊したエンジニアの眼の前をすり抜ける。だからこそ、アプリケーションの起動時、あるいはデプロイ直後のイミディエイトウィンドウで一撃のもとにテーブル定義の正当性を証明できるテストハーネスが必要なのだ。
—
2. 設計思想:DAOのライフサイクルとメモリ最適化の極意
Access VBAでデータベース構造を操作・検査する際、最も恐れなければならないのは「オブジェクトの解放漏れとキャッシュの肥大化」である。
`CurrentDb` を安易に何度も呼び出したり、`TableDef` や `Field` オブジェクトを適切に `Nothing` 代解放していなければ、Accessの内部エンジン(ACE)はメモリ内にゴーストオブジェクトを溜め込み、やがて「リソース不足」エラーを引き起こす。
プロのコードは、常に以下の鉄則に従う。
1. `CurrentDb` は変数に一度だけキャッシュする(毎回呼び出すと別インスタンスが生成され、メモリリークとパフォーマンス低下の元となる)。
2. 生成したオブジェクトは、エラーハンドリングのいかんに関わらず必ず逆順で明示的に解放する。
3. エラーを握り潰さず、アサーション(検証)結果を構造化して返す。
—
3. 実装コード:テーブル定義自動検証エンジン
以下のコードは、指定されたテーブルに「期待されるフィールドが存在するか」「データ型は一致しているか」「サイズは正しいか」「必須・インデックス設定はどうなっているか」を完全網羅して検証する、プロダクション品質のユニットテストクラス(あるいは標準モジュール)である。
Option Compare Database
Option Explicit
‘ =========================================================================
‘ 構造体定義:検証仕様を保持するDTO
‘ =========================================================================
Public Type FieldSpec
Name As String
DataType As Long WbDAO.DataTypeEnumに対応
Size As Long
Required As Boolean
AllowZeroLength As Boolean
End Type
‘ =========================================================================
‘ メインテストランナー:テーブル定義の整合性を検証する
‘ =========================================================================
Public Sub RunTableSchemaTest()
Dim db As DAO.Database
Dim isSuccess As Boolean
On Error GoTo ErrorHandler
‘ 鉄則: CurrentDbは必ず変数に格納し、多重インスタンス化を防ぐ
Set db = CurrentDb()
Debug.Print “=== [START] テーブル定義ユニットテスト ===”
isSuccess = True
‘ テストケース1: 受注マスター (T_OrderMaster) の検証
If Not VerifyTableExists(db, “T_OrderMaster”) Then
Debug.Print “[FAIL] 必須テーブル ‘T_OrderMaster’ が存在しません。”
isSuccess = False
Else
‘ フィールド仕様の検証
isSuccess = VerifyOrderMasterFields(db) And isSuccess
‘ インデックスの検証
isSuccess = VerifyOrderMasterIndexes(db) And isSuccess
End If
‘ テストケース2: 顧客マスター (T_Customer) の検証
If Not VerifyTableExists(db, “T_Customer”) Then
Debug.Print “[FAIL] 必須テーブル ‘T_Customer’ が存在しません。”
isSuccess = False
Else
isSuccess = VerifyCustomerFields(db) And isSuccess
End If
Debug.Print “========================================”
If isSuccess {
Debug.Print “RESULT: ALL TESTS PASSED. 構造の整合性は完全に担保されています。”
} Else {
Debug.Print “RESULT: TEST FAILED. スキーマに不整合があります。ログを確認してください。”
}
CleanUp:
‘ 確実にオブジェクトを解放する
If Not db Is Nothing Then Set db = Nothing
Exit Sub
ErrorHandler:
Debug.Print “[FATAL ERROR] 予期せぬエラーが発生しました: ” & Err.Description
isSuccess = False
Resume CleanUp
End Sub
‘ =========================================================================
‘ 補助関数:テーブル存在確認
‘ =========================================================================
Private Function VerifyTableExists(ByVal db As DAO.Database, ByVal tableName As String) As Boolean
Dim tdf As DAO.TableDef
Dim exists As Boolean
On Error Resume Next
Set tdf = db.TableDefs(tableName)
exists = (Err.Number = 0)
On Error GoTo 0
If Not tdf Is Nothing Then Set tdf = Nothing
VerifyTableExists = exists
End Function
‘ =========================================================================
‘ 個別テーブル検証:T_OrderMaster フィールド仕様
‘ =========================================================================
Private Function VerifyOrderMasterFields(ByVal db As DAO.Database) As Boolean
Dim tdf As DAO.TableDef
Dim fld As DAO.Field
Dim result As Boolean
Set tdf = db.TableDefs(“T_OrderMaster”)
result = True
‘ 1. OrderID (Long, Required)
result = CheckField(tdf, “OrderID”, dbLong, 4, True, False) And result
‘ 2. CustomerCode (Text, Length 20, Required)
result = CheckField(tdf, “CustomerCode”, dbText, 20, True, False) And result
‘ 3. OrderDate (Date/Time)
result = CheckField(tdf, “OrderDate”, dbDate, 8, True, False) And result
‘ 4. Memo (Memo/LongText, AllowZeroLength)
result = CheckField(tdf, “Memo”, dbMemo, 0, False, True) And result
VerifyOrderMasterFields = result
CleanUp:
If Not tdf Is Nothing Then Set tdf = Nothing
End Function
‘ =========================================================================
‘ 個別テーブル検証:T_Customer フィールド仕様
‘ =========================================================================
Private Function VerifyCustomerFields(ByVal db As DAO.Database) As Boolean
Dim tdf As DAO.TableDef
Dim result As Boolean
Set tdf = db.TableDefs(“T_Customer”)
result = True
result = CheckField(tdf, “CustomerCode”, dbText, 20, True, False) And result
result = CheckField(tdf, “CustomerName”, dbText, 100, True, False) And result
VerifyCustomerFields = result
CleanUp:
If Not tdf Is Nothing Then Set tdf = Nothing
End Function
‘ =========================================================================
‘ 核心検証エンジン:個別フィールドのアサーション
‘ =========================================================================
Private Function CheckField(ByVal tdf As DAO.TableDef, _
ByVal fieldName As String, _
ByVal expectedType As Long, _
ByVal expectedSize As Long, _
ByVal expectedRequired As Boolean, _
ByVal expectedAllowZero As Boolean) As Boolean
Dim fld As DAO.Field
Dim isValid As Boolean
isValid = True
On Error GoTo FieldNotFound
Set fld = tdf.Fields(fieldName)
‘ データ型チェック
If fld.Type <> expectedType Then
Debug.Print ” [MISMATCH] ” & tdf.Name & “.” & fieldName & ” 型が一致しません。期待値: ” & expectedType & “, 実際: ” & fld.Type
isValid = False
End If
‘ サイズチェック(テキスト型などの場合)
If expectedType = dbText Then
If fld.Size <> expectedSize Then
Debug.Print ” [MISMATCH] ” & tdf.Name & “.” & fieldName & ” サイズが一致しません。期待値: ” & expectedSize & “, 実際: ” & fld.Size
isValid = False
End If
End If
‘ 必須フラグチェック
If fld.Required <> expectedRequired Then
Debug.Print ” [MISMATCH] ” & tdf.Name & “.” & fieldName & ” 必須設定(Required)が一致しません。期待値: ” & expectedRequired & “, 実際: ” & fld.Required
isValid = False
End If
‘ ゼロ長文字列許可チェック
If expectedType = dbText Or expectedType = dbMemo Then
If fld.AllowZeroLength <> expectedAllowZero Then
Debug.Print ” [MISMATCH] ” & tdf.Name & “.” & fieldName & ” ゼロ長文字列許可が一致しません。期待値: ” & expectedAllowZero & “, 実際: ” & fld.AllowZeroLength
isValid = False
End If
End If
GoTo CleanUp
FieldNotFound:
Debug.Print ” [MISSING] フィールド ‘” & fieldName & “‘ がテーブル ‘” & tdf.Name & “‘ に存在しません。”
isValid = False
CleanUp:
If Not fld Is Nothing Then Set fld = Nothing
CheckField = isValid
End Function
‘ =========================================================================
‘ インデックス検証の例
‘ =========================================================================
Private Function VerifyOrderMasterIndexes(ByVal db As DAO.Database) As Boolean
Dim tdf As DAO.TableDef
Dim idx As DAO.Index
Dim idxFound As Boolean
Set tdf = db.TableDefs(“T_OrderMaster”)
idxFound = False
‘ 主キー(PrimaryKey)の存在と構成をチェック
For Each idx In tdf.Indexes
If idx.Primary Then
If idx.Fields = “OrderID” Then
idxFound = True
Exit For
End If
End If
Next idx
If Not idxFound Then
Debug.Print ” [MISMATCH] T_OrderMaster の主キー設定(OrderID)が不正、または存在しません。”
VerifyOrderMasterIndexes = False
Else
VerifyOrderMasterIndexes = True
End If
CleanUp:
If Not idx Is Nothing Then Set idx = Nothing
If Not tdf Is Nothing Then Set tdf = Nothing
End Function
—
4. このアーキテクチャがもたらす現場の優位性
1. デプロイメントの「完全な自動ゲートキーパー」
本番環境へACCDEファイルを配布する前に、この `RunTableSchemaTest` をCI/CDパイプラインの代わりとして、あるいはマスター初期化フォームのロード時に自動実行させる。これにより、構造的欠陥を持つアプリケーションが本番稼働するのを物理的に阻止できる。
2. レガシーシステムにおけるリファクタリングの恐怖からの解放
「この古いテーブル、どこからどこまでが必須なんだっけ?」という不毛な調査時間はゼロになる。コードが仕様書であり、仕様書がテストそのものであるため、ドキュメントの陳腐化とも無縁でいられる。
3. メモリ最適化の徹底
すべての関数において `TableDef` や `Field` のインスタンスループ抜け時に変数を明示的に `Set … = Nothing` で解放している。長時間の連続バッチ処理や大規模なスキーマ解析であっても、Accessのメモリリークを完全に封じ込める設計だ。
—
5. チーフアーキテクトからの提言
Accessを「おもちゃのデータベース」と揶揄する者がいる。しかし、それはAccessのポテンシャルを引き出せていない素人の戯言に過ぎない。適切なアーキテクチャと、メモリ管理の鉄則、そして今回紹介したような「構造の自己検証メカニズム」を実装すれば、Accessは基幹システムの末端としても、堅牢なデータストアとして十分に機能する。
コードは嘘をつかない。テーブル定義もまた、コードによって守られてこそ初めて信頼に値する。
明日からの開発に、ぜひこの「スキーマ・ユニットテスト」を導入し、あなたのシステムから「構造起因のバグ」を永遠に駆逐してほしい。
