画面変更の不毛な作業に別れを告げよ:Access DBをメタデータ駆動型UIへと進化させる極意
業務ツールの開発現場において、フィールド名の変更や項目の日本語表示名(和名)の修正要求が上がった際、データベースの定義を直し、クエリを直し、フォーム上のラベルを一つひとつ手作業で打ち直していないだろうか?
その手法は今すぐ捨ててほしい。
画面上のラベルとDBのフィールド定義が二重管理されている状態は、ヒューマンエラーの温床であり、保守コストを肥大化させる典型的なアンチパターンだ。
真のシステムアーキテクトは、「シングル・ソース・オブ・トゥルース(信頼できる唯一の情報源:SSOT)」 を構築する。Accessにおいて、その答えはテーブル定義の「説明(Description)プロパティ」にある。
本稿では、テーブルの「説明」プロパティをメタデータとして活用し、フォーム生成・読み込み時にラベルを動的書き換えする堅牢なメカニズムを解説する。DAOの隠れた罠を回避し、大規模アプリケーションでも瞬時に動作するプロダクションレベルのコードを伝授しよう。
—
1. DAOオブジェクトの罠:なぜ `Properties(“Description”)` はエラーを吐くのか?
一見すると、`TableDef.Fields(“フィールド名”).Properties(“Description”).Value` を取得するだけで済むように思える。しかし、ナイーブにこのコードを書いた者は、必ず「エラー 3270:プロパティが見つかりません」 という壁にぶち当たる。
DAOの設計思想と「存在しないプロパティ」
AccessのDAO(Data Access Objects)において、組み込みプロパティ(NameやTypeなど)以外のオプショナルなプロパティ(DescriptionやFormatなど)は、ユーザーがUI上またはコードで一度も値を設定していない場合、Propertyオブジェクト自体がメタデータコレクション内に存在しないという特殊な挙動を示す。
つまり、「値が空(””)」なのではなく、「プロパティという概念そのものがまだ生成されていない」のだ。
【間違い易いイメージ】
Field.Properties(“Description”).Value = “” (空文字)
【実際のDAO内部構造】
Field.Properties コレクション内に “Description” というキー自体が存在しない
⇒ 参照した瞬間に実行時エラー 3270 が発生!
したがって、堅牢なプロダクションコードを組むためには、以下のいずれかの設計が必須となる。
1. エラーハンドリングによる安全なフォールバック処理
2. プロパティコレクションの走査判定
3. プロパティが存在しない場合に動的に生成(CreateProperty)する防御的アプローチ
—
2. アーキテクチャ設計:メタデータエンジンとUIリフレクターの分離
保守性の高いツールを作成するため、処理を2つの責務に分離(関心事の分離)する。
1. `mdl_MetadataEngine`:
テーブル構造とフィールドの「説明」プロパティを安全に抽出し、メモリ内に効率よくキャッシュする層。
2. `mdl_DynamicUIReflector`:
フォーム上のコントロール(TextBoxやComboBox等)の `ControlSource`(コントロールソース)を解釈し、紐付くラベルの `Caption` を動的に書き換えるUI制御層。
ラベル紐付けの自動判定アルゴリズム
フォーム上のラベルを特定するには、次の2つのアプローチを組み合わせるのが最も堅牢だ。
- アプローチA(附属ラベル): テキストボックス等に直接紐付いている子ラベル(`ctl.Controls(0)`)を更新する。
- アプローチB(命名規則): テキストボックス `txt_顧客名` に対し、独立したラベル `lbl_顧客名` が配置されている場合、命名規約(`lbl_` + フィールド名)でマッピングする。
本実装では、この両方に対応したシームレスな解決ロジックを組み込む。
—
3. 完全実装:プロダクションレベルのVBAコード
以下のコードをそれぞれの標準モジュールに貼り付けて使用してほしい。
① メタデータ抽出エンジン (`mdl_MetadataEngine`)
Option Compare Database
Option Explicit
‘ メモリキャッシュ用(フォーム開閉時のDAOオーバーヘッドを極小化する)
Private m_MetadataCache As Object
”’
”’
”’ 対象のテーブル名
”’ 対象のフィールド名
”’
Public Function GetFieldDescription(ByVal tableName As String, ByVal fieldName As String) As String
On Error GoTo ErrorHandler
‘ キャッシュの初期化
If m_MetadataCache Is Nothing Then
Set m_MetadataCache = CreateObject(“Scripting.Dictionary”)
End If
Dim cacheKey As String
cacheKey = tableName & “.” & fieldName
‘ キャッシュに存在すれば高速に返す
If m_MetadataCache.Exists(cacheKey) Then
GetFieldDescription = m_MetadataCache(cacheKey)
Exit Function
End If
Dim db As DAO.Database
Dim tdf As DAO.TableDef
Dim fld As DAO.Field
Dim descValue As String
Set db = CurrentDb
‘ テーブルの存在確認
On Error Resume Next
Set tdf = db.TableDefs(tableName)
On Error GoTo ErrorHandler
If tdf Is Nothing Then
‘ リンクテーブルやクエリの可能性を考慮し、フォールバック
GetFieldDescription = fieldName
Exit Function
End If
‘ フィールドの存在確認
On Error Resume Next
Set fld = tdf.Fields(fieldName)
On Error GoTo ErrorHandler
If fld Is Nothing Then
GetFieldDescription = fieldName
Exit Function
End If
‘ Descriptionプロパティの安全な取得
On Error Resume Next
descValue = fld.Properties(“Description”).Value
If Err.Number = 3270 Then ‘ 3270: プロパティが見つかりません
Err.Clear
descValue = fieldName ‘ 設定がない場合はフィールド名をデフォルト値とする
ElseIf Err.Number <> 0 Then
On Error GoTo ErrorHandler
Err.Raise Err.Number
End If
On Error GoTo ErrorHandler
‘ 値が空文字の場合もフィールド名でフォールバック
If Trim$(descValue) = “” Then descValue = fieldName
‘ キャッシュに保存
m_MetadataCache(cacheKey) = descValue
GetFieldDescription = descValue
CleanUp:
Set fld = Nothing
Set tdf = Nothing
Set db = Nothing
Exit Function
ErrorHandler:
‘ 異常発生時は安全にフィールド名を返却し、システムを止めない
GetFieldDescription = fieldName
Resume CleanUp
End Function
”’
”’
Public Sub ClearMetadataCache()
If Not m_MetadataCache Is Nothing Then
m_MetadataCache.RemoveAll
Set m_MetadataCache = Nothing
End If
End Sub
—
② UIリフレクションモジュール (`mdl_DynamicUIReflector`)
Option Compare Database
Option Explicit
”’
”’
”’ 対象のフォームオブジェクト (Me を渡す)
Public Sub ReflectTableDefToFormLabels(ByRef targetForm As Access.Form)
On Error GoTo ErrorHandler
Dim recordSrc As String
recordSrc = targetForm.RecordSource
‘ レコードソースが未設定の場合は処理スキップ
If Trim$(recordSrc) = “” Then Exit Sub
‘ レコードソースがSQLやクエリの場合は、主となるテーブル名を抽出(簡易判定)
Dim tableName As String
tableName = ExtractTableName(recordSrc)
If tableName = “” Then Exit Sub
Dim ctl As Access.Control
Dim fldName As String
Dim desc As String
Dim parentLabel As Access.Label
‘ フォーム内の全コントロールを走査
For Each ctl In targetForm.Controls
‘ コントロールソースを持つコントロール(TextBox, ComboBox, CheckBox等)を対象とする
If HasProperty(ctl, “ControlSource”) Then
fldName = ctl.ControlSource
‘ バインドされているフィールドが存在し、式(=から始まる)でない場合
If fldName <> “” And Not (fldName Like “=” ) Then
‘ メタデータエンジンから「説明」を取得
desc = GetFieldDescription(tableName, fldName)
‘ — パターンA: 付属ラベル (Attached Label) の更新 —
On Error Resume Next
Set parentLabel = ctl.Controls(0)
If Err.Number = 0 And Not parentLabel Is Nothing Then
If parentLabel.ControlType = acLabel Then
parentLabel.Caption = desc
End If
End If
Err.Clear
On Error GoTo ErrorHandler
‘ — パターンB: 命名規則による独立ラベル (lbl_フィールド名) の更新 —
Dim targetLabelName As String
targetLabelName = “lbl_” & fldName
If ControlExists(targetForm, targetLabelName) Then
If targetForm.Controls(targetLabelName).ControlType = acLabel Then
targetForm.Controls(targetLabelName).Caption = desc
End If
End If
End If
End If
Next ctl
CleanUp:
Set ctl = Nothing
Set parentLabel = Nothing
Exit Sub
ErrorHandler:
Debug.Print “ReflectTableDefToFormLabels Error: ” & Err.Number & ” – ” & Err.Description
Resume CleanUp
End Sub
‘ — 補助関数群 —
”’
”’
Private Function ExtractTableName(ByVal src As String) As String
src = Trim$(src)
‘ 簡易的なSQL判定 (SELECT … FROM TableName)
If InStr(1, src, “SELECT “, vbTextCompare) = 1 Then
Dim fromPos As Long
fromPos = InStr(1, src, ” FROM “, vbTextCompare)
If fromPos > 0 Then
Dim afterFrom As String
afterFrom = Trim$(Mid$(src, fromPos + 6))
‘ 空白やセミコロン、JOIN等の前までをテーブル名とみなす
Dim endPos As Long
endPos = InStr(afterFrom, ” “)
If endPos = 0 Then endPos = InStr(afterFrom, “;”)
If endPos = 0 Then endPos = Len(afterFrom) + 1
ExtractTableName = Replace(Replace(Mid$(afterFrom, 1, endPos – 1), “[“, “”), “]”, “”)
Exit Function
End If
End If
‘ SQLでなければそのままテーブル名(またはクエリ名)として返す
ExtractTableName = Replace(Replace(src, “[“, “”), “]”, “”)
End Function
”’
”’
Private Function HasProperty(ByVal obj As Object, ByVal propName As String) As Boolean
On Error Resume Next
Dim dummy As Variant
dummy = CallByName(obj, propName, VbGet)
HasProperty = (Err.Number = 0)
Err.Clear
End Function
”’
”’
Private Function ControlExists(ByRef frm As Access.Form, ByVal ctlName As String) As Boolean
On Error Resume Next
Dim dummy As String
dummy = frm.Controls(ctlName).Name
ControlExists = (Err.Number = 0)
Err.Clear
End Function
—
4. 現場での組み込み手順(使い方)
使い方は極めてシンプルだ。動的反映を行いたいフォームの `Form_Load` イベントに、以下のたった1行を追加するだけで完了する。
Private Sub Form_Load()
‘ フォームの読み込み時にテーブル定義の「説明」をラベルへ自動反映
ReflectTableDefToFormLabels Me
End Sub
動作確認のテストシナリオ
1. テーブル `T_Customer` のフィールド `CustomerCode` の「説明」プロパティに 「顧客識別コード(必須)」 と入力して保存する。
2. フォームのテキストボックスの `ControlSource` を `CustomerCode` に設定する。
3. フォームを開く。⇒ 付属ラベルの表示が自動的に 「顧客識別コード(必須)」 に変更される。
4. テーブル定義の「説明」を 「お得意様コード」 に書き換えて保存し、再度フォームを開く。
5. VBAコードやフォームデザインを一切触ることなく、表示が 「お得意様コード」 に即座に追従する。
—
5. フロントエンド/バックエンド分離(アタッチテーブル)環境での注意点
大規模な実務運用では、DBが「操作用フロントエンド(.accdb)」と「データ保持用バックエンド(_be.accdb)」に分離されているケースが大半だ。この環境下でアタッチテーブル(リンクテーブル)を操作する場合、以下の技術的制約に注意しなければならない。
リンクテーブルにおける「説明」プロパティの挙動
Accessでテーブルをアタッチした場合、バックエンド側の `TableDef` に設定された `Description` プロパティは、フロントエンド側の `TableDef.Fields` コレクションには自動継承されない場合がある。
【解決策】
フロントエンド側でバックエンドのDBファイルを一時的に直接参照するか、アタッチ時にメタデータをローカルの管理用テーブルに読み込んでおく設計をとる。
バックエンドから直接取得する場合の補正コード(抜粋):
‘ リンクテーブルの接続先パス(Connectプロパティ)からバックエンドDBを直接参照して取得する
Dim linkDbPath As String
linkDbPath = Mid$(tdf.Connect, InStr(1, tdf.Connect, “DATABASE=”) + 9)
Dim backendDb As DAO.Database
Set backendDb = OpenDatabase(linkDbPath)
‘ バックエンドDB側のTableDefからDescriptionを取得…
本番運用では、システムの起動時にバックエンドからフィールドメタデータ一式をフロントエンド内の「隠しキャッシュテーブル」に一括インポートし、UI反映時にはそのローカルテーブルを参照する構成にすると、ネットワークIOが最小化され圧倒的なレスポンスを実現できる。
—
まとめ:メタデータ駆動がもたらす開発保守の革命
今回構築した仕組みは、単に「ラベルのテキストを入力する手間を省く」というレベルの話ではない。
1. 仕様変更に強い柔軟性: 仕様変更時の改修ポイントが「テーブル定義」の一箇所に集約される(SSOTの確立)。
2. 表記ゆれの完全撲滅: 画面ごとに「顧客コード」「顧客CD」「得意先コード」といった表記の表記揺れが発生するのを根本から防止する。
3. 開発スピードの爆発的向上: デザイナーでフォームを起こす際、ラベルの文字打ち作業からエンジニアが解放される。
VBAを単なる「マクロの延長」として扱うか、「堅牢なアプリケーション基盤」として設計するか。その差は、こうしたメタデータの扱い方とオブジェクトモデルへの深い洞察に現れる。
プロフェッショナルなエンジニアとして、ぜひあなたのプロジェクトにもこの「メタデータ駆動型UI生成」を導入し、美しく保守性の高いコード資産を構築してほしい。
