【テクニカル・上級編】【プロ】メタデータ駆動型開発:Excel定義書からテーブルを自動生成するエンジン – Access VBA解析バイブル

スポンサーリンク

メタデータ駆動型開発:Excel定義書からテーブルを自動生成するエンジン

レガシーシステムの改修や、アドホックなデータ収集要件が頻発する現場において、Accessの真価は「いかに素早くデータ構造を構築・変更できるか」にある。しかし、GUIによるテーブル定義の修正、フィールド型の設定、インデックスの貼付、そしてリレーションシップの再構築という一連の作業は、ヒューマンエラーの温床であり、エンジニアの精神すり減らす無間地獄だ。

真にスケーラブルなシステムを目指すならば、設計と実装の乖離を根本から断つべきだ。「テーブル定義書(Excel)がそのまま実体(Access Database)になる」――すなわち、メタデータ駆動型アーキテクチャ(Metadata-Driven Architecture)の構築こそが、この泥沼から抜け出す唯一の解である。

今回は、DAO(Data Access Objects)のライフサイクル、COMオブジェクトのメモリ管理、そして暗黙のJet/ACEエンジン挙動を完全に掌握したプロフェッショナルだけが実装できる、Excel定義書自動生成エンジンの極限の知見を公開する。

1. アーキテクチャの設計思想:なぜDAOか、なぜメタデータか

多くのアマチュアプログラマは、テーブル作成にDoCmd.RunSQLやADO(DMO)を使用する。しかし、Accessの内部構造を熟知したシニアエンジニアであれば、迷わずDAO(Data Access Objects)を選択する。

ADOは万能のデータアクセスメカニズムだが、Access(Jet/ACEエンジン)のネイティブ機能(リレーションシップの完全なプロパティ制御、きめ細やかなフィールド属性、隠しプロパティの設定など)にアクセスする際、ADOはあまりにも抽象化されすぎており、かつオーバーヘッドが大きい。一方、DAOはJet/ACEエンジンの直上に位置する。メモリ効率、速度、そしてテーブル定義のメタデータ操作において、DAOに並ぶ選択肢はない。

メタデータ駆動の要件

Excelを「単なる表計算ソフト」ではなく、「リレーショナルデータベースのスキーマ定義リポジトリ」として扱う。

  • Tablesシート: テーブル物理名、論理名、オプション
  • Fieldsシート: 対象テーブル名、フィールド物理名、論理名、データ型、サイズ、必須、既定値、インデックス
  • Relationsシート: リレーション名、親テーブル、親フィールド、子テーブル、子フィールド、カスケード設定

2. 実装:Excel駆動型テーブル自動生成エンジン

以下に提示するコードは、単に動くだけのスクリプトではない。

  • トランザクション管理: 定義途中の不整合による半端なテーブル作成を防ぐ。
  • 完全なオブジェクト解放: DAOオブジェクトの連鎖参照によるメモリリーク(Access特有のクラッシュ原因)を確実に潰す。
  • 既存構造の安全なアタッチメント: 既存テーブルの安全な破棄と再構築、あるいはスキーマの差分適用の思想。

コアエンジンモジュール(VBA)

Option Compare Database
Option Explicit

‘ =========================================================================
‘ 権威あるチーフアーキテクトによる極限のDAOテーブル自動生成エンジン
‘ =========================================================================
Public Sub BuildDatabaseFromMetadata(ByVal excelPath As String)
Dim xlApp As Object
Dim xlWb As Object
Dim db As DAO.Database
Dim wsTables As DAO.Workspace

‘ エラーハンドリングの要:トランザクションとCOM解放を担保
On Error GoTo ErrorHandler

‘ 1. DAOデータベースの参照取得(現在のカレントDB)
Set db = CurrentDb
Set wsTables = DBEngine.Workspaces(0)

‘ 2. 外部Excelを完全非表示・Late Bindingで安全に起動
Set xlApp = CreateObject(“Excel.Application”)
xlApp.Visible = False
xlApp.DisplayAlerts = False
Set xlWb = xlApp.Workbooks.Open(excelPath, ReadOnly:=True)

‘ 3. トランザクション開始(DDLのロールバックを制御)
wsTables.BeginTrans

‘ ステップA: リレーションシップの事前削除(外部キー制約の競合回避)
Call DropAllUserRelations(db)

‘ ステップB: テーブルとフィールドの動的構築
Call CreateTablesAndFields(db, xlWb.Sheets(“Fields”), xlWb.Sheets(“Tables”))

‘ ステップC: リレーションシップの再構築
Call CreateRelations(db, xlWb.Sheets(“Relations”))

‘ コミット
wsTables.CommitTrans

MsgBox “メタデータ駆動型ビルドが正常に完了しました。”, vbInformation, “アーキテクチャ・エンジン”
GoTo SafeExit

ErrorHandler:
‘ 異常発生時はロールバック
wsTables.Rollback
MsgBox “ビルドエラー発生 (Error ” & Err.Number & “): ” & Err.Description, vbCritical, “致命的エラー”

SafeExit:
‘ 4. 厳格なオブジェクトの破棄(メモリリークの完全防止)
On Error Resume Next
If Not xlWb Is Nothing Then xlWb.Close False
If Not xlApp Is Nothing Then xlApp.Quit
Set xlWb = Nothing
Set xlApp = Nothing
Set db = Nothing
Set wsTables = Nothing
On Error GoTo 0
End Sub

‘ =========================================================================
‘ 内部サブルーチン:テーブルとフィールドの生成
‘ =========================================================================
Private Sub CreateTablesAndFields(ByRef db As DAO.Database, ByRef shtFields As Object, ByRef shtTables As Object)
Dim tdf As DAO.TableDef
Dim fld As DAO.Field
Dim lastRowT As Long, lastRowF As Long
Dim i As Long, r As Long
Dim currentTable As String
Dim tblName As String

lastRowT = shtTables.Cells(shtTables.Rows.Count, “A”).End(-4162).Row ‘ xlUp = -4162
lastRowF = shtFields.Cells(shtFields.Rows.Count, “A”).End(-4162).Row

‘ テーブルのループ処理
For i = 2 To lastRowT
tblName = shtTables.Cells(i, 1).Value
If Len(Trim(tblName)) > 0 then
‘ 既存テーブルが存在する場合は削除(※プロダクション環境では差分更新を推奨)
On Error Resume Next
db.TableDefs.Delete tblName
On Error GoTo ErrorHandler

‘ TableDefオブジェクトの生成
Set tdf = db.CreateTableDef(tblName)

‘ Fieldsシートから該当テーブルのフィールドを紐付け
For r = 2 To lastRowF
If shtFields.Cells(r, 1).Value = tblName Then
Dim fldName As String
Dim fldType As Integer
Dim fldSize As Long
Dim isReq As Boolean

fldName = shtFields.Cells(r, 2).Value
fldType = GetDaoType(shtFields.Cells(r, 3).Value)
fldSize = Val(shtFields.Cells(r, 4).Value)
isReq = CBool(shtFields.Cells(r, 5).Value)

Set fld = tdf.CreateField(fldName, fldType)

‘ サイズプロパティ設定(テキスト型等の場合のみ有効)
If fldType = dbText And fldSize > 0 Then
fld.Size = fldSize
End If

‘ 必須プロパティ
If isReq Then
fld.Required = True
End If

tdf.Fields.Append fld
Set fld = Nothing
End If
Next r

‘ データベースにテーブルを追加
db.TableDefs.Append tdf
Set tdf = Nothing
End If
Next i
Exit Sub

ErrorHandler:
Err.Raise Err.Number, “CreateTablesAndFields”, Err.Description
End Sub

‘ =========================================================================
‘ 文字列型からDAOデータ型へのマッピング
‘ =========================================================================
Private Function GetDaoType(ByVal typeName As String) As Integer
Select Case UCase(Trim(typeName))
Case “LONG”, “INTEGER”, “INT”: GetDaoType = dbLong
Case “TEXT”, “STRING”, “VARCHAR”: GetDaoType = dbText
Case “MEMO”, “LONGTEXT”: GetDaoType = dbMemo
Case “DATE”, “DATETIME”: GetDaoType = dbDate
Case “CURRENCY”, “MONEY”: GetDaoType = dbCurrency
Case “BOOLEAN”, “BOOL”, “BYTE”: GetDaoType = dbBoolean
Case “DOUBLE”: GetDaoType = dbDouble
Case Else: GetDaoType = dbText ‘ フォールバック
End Select
End Function

‘ =========================================================================
‘ リレーションシップの構築
‘ =========================================================================
Private Sub CreateRelations(ByRef db As DAO.Database, ByRef shtRel As Object)
Dim rel As DAO.Relation
Dim lastRow As Long
Dim i As Long

lastRow = shtRel.Cells(shtRel.Rows.Count, “A”).End(-4162).Row

For i = 2 To lastRow
Dim relName As String, primaryTbl As String, foreignTbl As String
Dim primaryCol As String, foreignCol As String

relName = shtRel.Cells(i, 1).Value
primaryTbl = shtRel.Cells(i, 2).Value
primaryCol = shtRel.Cells(i, 3).Value
foreignTbl = shtRel.Cells(i, 4).Value
foreignCol = shtRel.Cells(i, 5).Value

If Len(Trim(relName)) > 0 Then
Set rel = db.CreateRelation(relName, primaryTbl, foreignTbl, dbRelationUpdateCascade)
rel.Attributes = dbRelationUpdateCascade ‘ 必要に応じ dbRelationDeleteCascade 等を追加

Dim fld As DAO.Field
Set fld = rel.CreateField(primaryCol)
fld.ForeignName = foreignCol
rel.Fields.Append fld

db.Relations.Append rel

Set fld = Nothing
Set rel = Nothing
End If
Next i
End Sub

‘ =========================================================================
‘ 既存リレーションの安全な全削除
‘ =========================================================================
Private Sub DropAllUserRelations(ByRef db As DAO.Database)
Dim rel As DAO.Relation
Dim i As Long

For i = db.Relations.Count – 1 To 0 Step -1
Set rel = db.Relations(i)
‘ システムリレーションを除外して削除
If Left(rel.Name, 4) <> “MSys” Then
db.Relations.Delete rel.Name
End If
Set rel = Nothing
Next i
End Sub

3. シニアエンジニアの知見:メモリ最適化とJet/ACEエンジンの罠

上記のコードを単に「動くスクリプト」で終わらせず、エンタープライズレベルの堅牢性を持たせるための重要知見を共有する。

1. COMオブジェクトの解放漏れと「目に見えないメモリリーク」

VBAで`CreateObject(“Excel.Application”)`を実行すると、背後で重厚なExcelプロセスが起動する。スクリプト終了時に`xlApp.Quit`を呼ぶだけでは不十分だ。

  • 変数の完全破棄: `Set xlApp = Nothing`を明示的に実行し、参照カウンターを確実に0に落とすこと。これを怠ると、タスクマネージャー上にゾンビプロセス(Excel.exe)が残り続け、次回の実行時にファイルロックやメモリ枯渇を引き起こす。
  • エラー時の確実なクリーンアップ: `On Error GoTo ErrorHandler`を配置し、例外発生時であっても確実にCOMオブジェクトが解放される構造を強制している。

2. トランザクション制御によるスキーマの原子性(Atomicity)

テーブル定義の変更中にエラーが発生した場合、「途中まで作成された中途半端なデータベース」が残るのが最もタチが悪い。
DAOの`Workspace.BeginTrans`と`CommitTrans` / `Rollback`を活用することで、DDL(Data Definition Language)操作であってもトランザクションの保護下におくことができる(※Jet/ACEエンジンのバージョンや操作内容によっては一部制限があるが、テーブル定義の連続追加においては極めて有効である)。

3. リレーションシップ順序のパラドックス

テーブルとリレーションを同時に構築しようとすると、存在しないテーブルに対するリレーションを張ろうとしてエラー(Error 3012: 「指定したリレーションは既に存在するか、または正しくありません」)が発生する。
そのため、本エンジンでは以下の厳格な順序を強制している。
1. 全リレーションの事前パージ(既存の依存関係を完全に断つ)
2. 全テーブル・フィールドの構築(すべての箱を確実に用意する)
3. リレーションの再構築(すべての箱が出揃った状態で結合を貼る)

4. 運用・保守におけるベストプラクティス

メタデータ駆動型開発を現場に導入するにあたり、以下のガバナンスを効かせるべきである。

  • バージョン管理の統合: Excel定義書自体をGit等のバージョン管理システムで管理する。これにより、「誰が、いつ、どの仕様変更を入れたか」の差分(Diff)が完全に追跡可能になる。
  • デプロイメントの自動化: アプリケーション起動時、またはインストーラー実行時に上記のVBAルーチンをサイレント実行することで、クライアント側のAccessバックエンドDBを常に最新のスキーマに自動マイグレーションさせることが可能となる。

GUIでの手動修正という「職人芸」の時代は終わった。
コードとメタデータでシステムを支配する者だけが、レガシーの呪縛から解放される。真のエンジニアリングを、あなたの現場に実装せよ。

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