静的スキーマという呪縛からの解放
RDBMS(リレーショナルデータベース)の基本原則は「堅牢な静的スキーマ」である。データベースはあらかじめ定義されたデータ型とフィールド長に基づき、厳密にデータを格納する。Microsoft AccessのストレージエンジンであるACE(Access Database Engine / 旧Jet)もまた、この原則に忠実に設計されている。
しかし、実務におけるデータ連携の現場は、この「美しき原則」を無残に破壊する。
- 「取引先システムが、事前の連絡なしにCSVの末尾に新しい分析用フィールドを追加してきた」
- 「複数のサブシステムから出力されるCSVで、微妙にカラム数が異なっているが、同一テーブルに集約して取り込みたい」
このような要求に対し、従来のAccess VBA開発者は「インポートエラーテーブル」の発生に頭を悩ませるか、あるいは「エラーを検知してシステムを緊急停止させる」という消極的な運用回避を選択してきた。
本稿で提示するのは、そのようなレガシーな諦めを排し、「未知のフィールドをインポート時に動的に検知し、テーブル定義(TableDef)をリアルタイムに自動拡張しながら、1トランザクションも落とさずにインポートを完遂する」極限のVBAアーキテクチャである。
単にフィールドを追加するだけではない。ACEエンジンのスキーマロック、`CurrentDb`が抱えるメモリリークとキャッシュの不整合、ADODBを用いた高信頼性I/O、そして大量データ処理時のパフォーマンス設計に至るまで、実務で戦うシニアエンジニアに必要なすべての技術要素を網羅して解説する。
—
アーキテクチャ設計:動的スキーマ拡張のライフサイクル
動的にスキーマを拡張しつつCSVを取り込む処理は、以下のライフサイクルに従って厳密に制御されなければならない。
[CSVファイルオープン (ADODB.Stream)]
│
[ヘッダー行の解析 (UTF-8 / Shift_JIS両対応)]
│
[既存テーブル定義(DAO.TableDef)との比較・照合]
│
┌────────┴────────┐
│(未知のフィールドを検出) │(すべて既知のフィールド)
▼ ▼
[排他ロックの確保] │
[DAO.Fieldの動的生成] │
[Fields.Appendによる拡張] │
[TableDefs.Refresh実行] │
└────────┬────────┘
│
[Recordsetによる高速データ投入 (dbOpenDynaset)]
│
[メモリ解放 & トランザクション確定]
1. スキーマロック(Schema Lock)の回避策
ACEエンジンは、テーブル構造の変更(DDL操作やDAOによる`Fields.Append`)を行う際、そのテーブルに対する排他ロックを要求する。
もし対象テーブルがフォームのレコードソースとして開かれていたり、別の非同期プロセス(あるいは解放漏れのRecordset)によって参照されていたりした場合、スキーマ変更は `エラー 3211: テーブル ‘<テーブル名>‘ は、ほかのユーザーまたはプロセスで使用されているため、ロックできません。` を吐いて即座にクラッシュする。
これを防ぐため、処理のライフサイクル初期において、対象テーブルに対する接続を完全にクローズし、排他モードでのオープンを試みる事前検証コードが必須となる。
2. `CurrentDb` と `DBEngine(0)(0)` の使い分け
多くのVBA開発者が無意識に使用する `CurrentDb` は、呼び出されるたびに新しい `DAO.Database` インスタンスを生成(およびキャッシュを破棄)するヘルパー関数である。
動的スキーマ変更を行う場合、`CurrentDb` を多用すると、追加したフィールドが即座に認識されないキャッシュの不整合や、メモリリークを引き起こす。
本設計では、明示的に `Set db = CurrentDb` でデータベースオブジェクトを単一の変数にバインドし、処理全体で同一のインスタンスを使い回す。さらに、スキーマ変更直後には `db.TableDefs.Refresh` を実行し、ACEエンジンの内部メタデータを強制的に同期させる。
—
極限のVBA実装:動的フィールド拡張インポートエンジン
以下に、実務でそのまま使用できる、極めて堅牢に設計された動的スキーマ拡張インポートスクリプトを示す。
このコードは、UTF-8/Shift_JIS双方に対応し、ダブルクォーテーションで囲まれた複雑なCSVを正しくパースした上で、未知のカラムを発見した場合は自動的に `Memo型(Long Text)` または `Text型(255文字)` としてテーブルを拡張する。
参照設定(References)
このコードを実行する前に、VBAエディタの「ツール」->「参照設定」から以下にチェックが入っていることを確認すること。
- Microsoft Office 16.0 Access database engine Object Library (またはそれ以降のバージョン)
- Microsoft ActiveX Data Objects 6.1 Library(ADODBを使用するため)
Option Compare Database
Option Explicit
‘ ==============================================================================
‘ 業務自動化モジュール: DynamicSchemaImporter
‘ 説明: CSVヘッダーを動的に解析し、既存のAccessテーブルに存在しないフィールドを
‘ 自動追加(TableDef拡張)した上で、高速にインポートを実行する。
‘ ==============================================================================
Public Sub ImportCsvWithDynamicSchema( _
ByVal strTableName As String, _
ByVal strFilePath As String, _
Optional ByVal strCharSet As String = “UTF-8”)
On Error GoTo Err_Handler
Dim db As DAO.Database
Dim tdf As DAO.TableDef
Dim rst As DAO.Recordset
Dim stream As ADODB.stream
Dim strHeaderLine As String
Dim strDataLine As String
Dim arrHeaders() As String
Dim arrFields() As String
Dim i As Long
Dim strColName As String
Dim colAddedFields As Collection
Set colAddedFields = New Collection
‘ 1. データベースオブジェクトの確実なバインド
Set db = CurrentDb()
‘ 対象テーブルの存在チェック。存在しない場合は新規作成
If Not TableExists(db, strTableName) Then
‘ 初期フィールド(IDオートナンバー)を持つテーブルを新規作成
Set tdf = db.CreateTableDef(strTableName)
Dim fldId As DAO.field
Set fldId = tdf.CreateField(“ID”, dbLong)
fldId.Attributes = dbAutoIncrField
tdf.Fields.Append fldId
db.TableDefs.Append tdf
db.TableDefs.Refresh
End If
Set tdf = db.TableDefs(strTableName)
‘ 2. ADODB.Streamによる高信頼性ファイルオープン
Set stream = New ADODB.stream
With stream
.Type = adTypeText
.Charset = strCharSet
.Open
.LoadFromFile strFilePath
End With
‘ 3. ヘッダー行の読み込みと解析
If stream.EOS Then
Err.Raise vbObjectError + 1001, “ImportCsvWithDynamicSchema”, “CSVファイルが空です。”
End If
strHeaderLine = stream.ReadText(adReadLine)
arrHeaders = ParseCsvLine(strHeaderLine)
‘ 4. スキーマの動的拡張(TableDef操作)
‘ スキーマ変更用の排他ロックチェックを兼ねてRefresh
db.TableDefs.Refresh
Dim fldExists As Boolean
Dim targetFld As DAO.field
For i = LBound(arrHeaders) To UBound(arrHeaders)
strColName = Trim$(arrHeaders(i))
‘ カラム名のクレンジング(Accessの制約文字を排除)
strColName = CleanFieldName(strColName)
fldExists = False
For Each targetFld In tdf.Fields
If StrComp(targetFld.Name, strColName, vbTextCompare) = 0 Then
fldExists = True
Exit For
End If
Next targetFld
‘ 未知のフィールドを発見した場合、動的に追加
If Not fldExists And Len(strColName) > 0 Then
‘ 実務上、動的追加フィールドは最大の汎用性を持つ「dbMemo (Long Text)」
‘ または「dbText (255)」として作成するのが安全。ここではdbMemoを採用。
Set targetFld = tdf.CreateField(strColName, dbMemo)
tdf.Fields.Append targetFld
colAddedFields.Add strColName
Debug.Print “フィールドを追加しました: ” & strColName
End If
Next i
‘ スキーマ変更を確定させるためのリフレッシュ
If colAddedFields.Count > 0 Then
tdf.Fields.Refresh
db.TableDefs.Refresh
End If
‘ 5. データインポート処理 (Recordsetによる高速バルク風インサート)
‘ dbOpenDynaset + dbAppendOnly を指定することで、既存レコードを読み込まず、
‘ 高速に書き込み専用バッファとしてRecordsetを開く(パフォーマンス向上)
Set rst = db.OpenRecordset(strTableName, dbOpenDynaset, dbAppendOnly)
‘ トランザクションの開始(ACEエンジンのログバッファを利用し高速化)
DBEngine.BeginTrans
Dim rowCount As Long
rowCount = 0
Do While Not stream.EOS
strDataLine = stream.ReadText(adReadLine)
If Len(Trim$(strDataLine)) > 0 Then
arrFields = ParseCsvLine(strDataLine)
rst.AddNew
‘ ヘッダーのインデックスとデータのインデックスをマッピングして格納
Dim maxCols As Long
maxCols = IIf(UBound(arrFields) < UBound(arrHeaders), UBound(arrFields), UBound(arrHeaders))
For i = 0 To maxCols
strColName = CleanFieldName(Trim$(arrHeaders(i)))
If Len(strColName) > 0 And Len(arrFields(i)) > 0 Then
‘ データ格納型変換エラーを回避しつつ代入
rst.Fields(strColName).Value = arrFields(i)
End If
Next i
rst.Update
rowCount = rowCount + 1
‘ 10,000行ごとに中間コミットを入れてロールバックセグメントの枯渇を防ぐ
If rowCount Mod 10000 = 0 Then
DBEngine.CommitTrans
DBEngine.BeginTrans
End If
End If
Loop
‘ 最終コミット
DBEngine.CommitTrans
Debug.Print “インポート完了: ” & rowCount & ” 件のレコードを処理しました。”
Exit_Handler:
‘ 6. オブジェクトライフサイクルの厳密な管理(メモリ解放)
On Error Resume Next
If Not rst Is Nothing Then
rst.Close
Set rst = Nothing
End If
If Not stream Is Nothing Then
stream.Close
Set stream = Nothing
End If
Set tdf = Nothing
Set db = Nothing
Exit Sub
Err_Handler:
‘ エラー発生時はトランザクションを確実にロールバック
On Error Resume Next
DBEngine.Rollback
MsgBox “致命的エラーが発生しました。” & vbCrLf & _
“エラー番号: ” & Err.Number & vbCrLf & _
“エラー内容: ” & Err.Description, vbCritical, “エラー”
Resume Exit_Handler
End Sub
‘ ==============================================================================
‘ 補助関数: TableExists
‘ 説明: 指定されたテーブルがデータベース内に存在するか確認する
‘ ==============================================================================
Private Function TableExists(ByRef db As DAO.Database, ByVal strTableName As String) As Boolean
Dim tdf As DAO.TableDef
On Error Resume Next
Set tdf = db.TableDefs(strTableName)
TableExists = (Err.Number = 0)
On Error GoTo 0
End Function
‘ ==============================================================================
‘ 補助関数: CleanFieldName
‘ 説明: Accessのフィールド名として使用不可能な文字をアンダースコアに置換する
‘ ==============================================================================
Private Function CleanFieldName(ByVal strInput As String) As String
Dim strResult As String
strResult = strInput
‘ Accessで禁忌とされる記号の置換
strResult = Replace(strResult, “.”, “_”)
strResult = Replace(strResult, “!”, “_”)
strResult = Replace(strResult, “`”, “_”)
strResult = Replace(strResult, “[“, “_”)
strResult = Replace(strResult, “]”, “_”)
strResult = Replace(strResult, ” “, “_”) ‘ スペースも実務上トラブルの元なので置換
CleanFieldName = Left$(strResult, 64) ‘ Accessのフィールド名上限は64文字
End Function
‘ ==============================================================================
‘ 補助関数: ParseCsvLine
‘ 説明: ダブルクォーテーションで囲まれたカンマや改行を考慮し、正確にCSVをパースする
‘ ==============================================================================
Private Function ParseCsvLine(ByVal strLine As String) As String()
Dim colFields As Collection
Set colFields = New Collection
Dim i As Long
Dim strChar As String
Dim inQuotes As Boolean
Dim strFieldValue As String
inQuotes = False
strFieldValue = “”
For i = 1 To Len(strLine)
strChar = Mid$(strLine, i, 1)
If strChar = “””” Then
‘ ダブルクォーテーションのエスケープ(””)対応
If i < Len(strLine) Then
If Mid$(strLine, i + 1, 1) = """" Then
strFieldValue = strFieldValue & """"
i = i + 1 ' 次のダブルクォーテーションをスキップ
Else
inQuotes = Not inQuotes
End If
Else
inQuotes = Not inQuotes
End If
ElseRef:
If strChar = "," And Not inQuotes Then
colFields.Add strFieldValue
strFieldValue = ""
Else
strFieldValue = strFieldValue & strChar
End If
End If
Next i
colFields.Add strFieldValue ' 最後のフィールドを追加
' コレクションを配列に変換して返す
Dim arrResult() As String
ReDim arrResult(0 To colFields.Count - 1)
For i = 1 To colFields.Count
arrResult(i - 1) = colFields(i)
Next i
ParseCsvLine = arrResult
End Function
---
深淵の技術解説(チーフアーキテクトの眼)
上記のコードが、なぜ一般的な「VBA入門書」に書かれているようなインポートコードと一線を画しているのか。そのアーキテクチャの思想を、ACEエンジンの深部仕様と紐解いて解説する。
1. `dbOpenDynaset` + `dbAppendOnly` の選択理由
DAOでRecordsetを開く際、多くの開発者は引数を省略してデフォルトの状態で開く。しかし、数万行を超えるインポートにおいて、デフォルトのレコードセットは「既存の全レコードをメモリ上にインデックス展開」しようとするため、極めて動作が重くなる。
`dbAppendOnly` オプションを指定して `Dynaset` を開くと、ACEエンジンは「新規追加専用の空のバッファ」のみを確保する。これにより、既存データが数百万件あろうとも、インポート時のメモリ使用量は一定かつ最小限に抑えられ、劇的な高速化を実現できる。
2. トランザクション境界のコントロールとロールバックセグメント
ACEエンジンにおける `DBEngine.BeginTrans` と `CommitTrans` の使用は、単なるデータの整合性確保(ロールバック機能)だけが目的ではない。
トランザクションを明示的に開始すると、ACEエンジンはディスクへの物理書き込み(I/O)を一時的に保留し、メモリ上のログバッファに対して一括して書き込みを行う。これにより、物理ディスクI/Oの発生回数が激減し、処理速度が 5倍〜20倍 向上する。
ただし、注意が必要なのは「ロールバックセグメントの限界」である。32bit環境やメモリ制約の厳しいレガシーなPCで、10万行を超えるデータを1つのトランザクションで抱え込むと、ACEエンジンのバッファが溢れ(エラー 3035: ‘System resources exceeded’)、システムがクラッシュする。
これを避けるため、コード内では `rowCount Mod 10000 = 0` のタイミングで中間コミットを挟み、メモリ領域を定期的に解放している。
3. データ型の自動推論(Type Inference)の落とし穴
CSVインポート時に動的にフィールドを追加する場合、そのデータ型を何にするかは極めて重要かつ厄介な問題である。
「最初の行が数字だったから `dbLong`(数値型)でフィールドを追加した」結果、1万行目に `A123` という文字列が出現した瞬間、型ミスマッチでインポートは破綻する。
実務レベルの動的インポートエンジンにおいては、「新規追加フィールドはすべて `dbMemo`(Long Text)として定義する」のが最も堅牢なプラクティスである。
Accessの `dbText` は255文字の制限があり、それを超えるデータが1件でも入ると切り捨て、あるいはエラーとなる。一方で `dbMemo` であれば、文字数制限の実質的な制約(最大2GB)から解放され、あらゆるデータ型(日付、数値、長文テキスト)を安全に一度受け止めることができる。
データが格納された後、クエリ(`CDate()` や `CDbl()` などの変換関数)を用いて適切なデータ型へキャストし、本番のデータウェアハウスへ流し込めばよい。
—
レガシーをモダンに昇華させるために
「Microsoft Accessは古い技術である」と切り捨てるのは容易だ。しかし、いまなお多くのエンタープライズの現場で、ローカルデータ処理の超高速ラピッドプロトタイピングツール、あるいはサブシステム間のデータクレンジングハブとしてAccess以上の利便性を持つツールは存在しない。
今回解説した「動的 TableDef 拡張インポート」は、Accessが持つ「デスクトップデータベースとしての柔軟性」を極限まで引き出したものである。静的なスキーマ設計の良さを理解した上で、あえて動的な拡張をコードで制御する。この柔軟性こそが、変化の激しい現代のビジネス要件に、レガシーと言われる技術で立ち向かうための強力な武器となる。
オブジェクトのライフサイクルをコントロールし、メモリの最後の一滴まで最適化する。その思想の根底にあるのは、システムへの圧倒的な理解と、技術への敬意である。
