【実務・中級編】ADOのFieldsコレクションを動的に操作する:汎用的なデータインポートツールの作り方 – Access VBA解析バイブル

スポンサーリンク
スポンサーリンク

脱・ハードコーディング。ADOのFieldsコレクションを極め、真の「汎用データインポートエンジン」を構築せよ

現場のエンジニアが陥りがちな最大の罠。それは、「仕様が変わるたびにコードを書き換える」という、再利用性の欠片もない実装だ。

「Aテーブルへのインポートはこう書く」「Bテーブルならこう書く」……。もし君が、`rs.Fields(“CustomerName”).Value = …` といったフィールド名を直接コードに書き込んでいるとしたら、君が作っているのは「ツール」ではない。単なる「使い捨てのスクリプト」だ。

真に価値のある業務自動化エンジニアが作るべきは、データの構造(メタデータ)を動的に解釈し、どんなテーブル構造にも適応できる「インポートエンジン」である。

本稿では、ADO(ActiveX Data Objects)の`Fields`コレクションを自在に操り、テーブル構造に依存しない、堅牢で汎用的なインポートプロシージャの設計思想と実装を伝授する。

1. なぜDAOではなくADOなのか? アーキテクチャの選択

Access開発において、DAO(Data Access Objects)はローカルなAccessエンジンを操作する上で極めて高速だ。しかし、「外部データとの連携」や「汎用的なデータ移動」を主眼に置くなら、選択肢は迷わずADOになる。

ADOが汎用ツールに適している理由

1. メタデータの抽象化: ADOの`Fields`コレクションは、接続先のデータソース(Excel, SQL Server, CSV, Accessなど)が何であれ、共通のインターフェースでフィールド情報にアクセスできる。
2. デカップリング(疎結合): ソース(送り元)とターゲット(送り先)の構造を、フィールド名という「共通言語」だけで紐付ける設計が可能になる。
3. トランザクション制御の柔軟性: 複数のデータソースをまたぐ、あるいは複雑なエラーリカバリを伴う処理において、ADOの接続オブジェクトによるトランザクション管理は極めて強力だ。

2. 設計思想:メタデータ駆動型インポート

ハードコーディングを排除するための鍵は、「フィールド名による動的マッピング」にある。

インポート処理のアルゴリズムを以下のように定義する。

1. ソース(Source)のレコードセットを走査する。
2. 各フィールドについて、その名前(Name)を取得する。
3. ターゲット(Target)のレコードセット(またはテーブル構造)を走査し、同じ名前のフィールドが存在するかを判定する。
4. 存在する場合のみ、値を流し込む。

この設計により、ソースに余分な列があっても、ターゲットに足りない列があっても、プログラムを一行も書き換えることなく動作し続ける。

3. プロダクション・コード:汎用インポートエンジンの実装

以下に、実務でそのまま利用可能なレベルの堅牢なコードを示す。単なるループ処理ではない。「トランザクションによる整合性確保」と「エラーハンドリングによるロールバック」を組み込んだ、プロフェッショナルな設計だ。

Option Compare Database
Option Explicit

''' <summary>
''' 汎用データインポートプロシージャ
''' ソースレコードセットからターゲットテーブルへ、フィールド名が一致するデータを転送する。
''' </summary>
''' <param name="conn">接続済みのADO.Connectionオブジェクト</param>
''' <param name="rsSource">データ供給元となるADO.Recordset</param>
''' <param name="strTargetTableName">書き込み先となるAccessテーブル名</param>
Public Sub GenericDataImporter(ByRef conn As ADODB.Connection, _
                               ByRef rsSource As ADODB.Recordset, _
                               ByVal strTargetTableName As String)
    
    Dim rsTarget As ADODB.Recordset
    Dim fldSrc As ADODB.Field
    Dim fldTgt As ADODB.Field
    Dim isMatched As Boolean
    Dim rowCount As Long
    
    ' 1. エラーハンドリングの開始
    On Error GoTo ErrorHandler

    ' 2. トランザクションの開始(全件成功か、全件失敗かの原子性を担保)
    conn.BeginTrans
    Debug.Print "--- Import Process Started ---"

    ' 3. ターゲットレコードセットのオープン
    ' ※SQLを使用することで、インポート先の制御を容易にする
    Set rsTarget = New ADODB.Recordset
    rsTarget.Open "SELECT * FROM [" & strTargetTableName & "]", conn, _
                  adOpenKeyset, adLockOptimistic

    ' 4. ソースデータの走査
    If rsSource.EOF Then
        Debug.Print "Source is empty. Nothing to import."
        GoTo CleanUp
    End If

    Do Until rsSource.EOF
        rsTarget.AddNew
        
        ' 5. フィールドのマッピング(ここが設計の肝)
        ' ソースの各フィールドに対してループ
        For Each fldSrc In rsSource.Fields
            isMatched = False
            
            ' ターゲット側に同名のフィールドが存在するか確認
            ' ※本来は事前にターゲットのフィールドリストをキャッシュしておくとより高速
            On Error Resume Next
            Set fldTgt = rsTarget.Fields(fldSrc.Name)
            If Err.Number = 0 Then
                isMatched = True
            End If
            On Error GoTo ErrorHandler ' エラーハンドリングを戻す
            
            ' 一致した場合のみ、値を転送(Null値の考慮も自動で行われる)
            If isMatched Then
                ' 型不一致によるエラーを避けるため、値の代入は慎重に行う
                ' 実務ではここで型変換ロジックを挟むことも検討すべき
                fldTgt.Value = fldSrc.Value
            End If
        Next fldSrc
        
        rsTarget.Update
        rowCount = rowCount + 1
        rsSource.MoveNext
    Loop

    ' 6. コミット(すべての処理が正常に完了した場合のみ)
    conn.CommitTrans
    Debug.Print "Successfully imported " & rowCount & " rows to " & strTargetTableName
    
CleanUp:
    ' オブジェクトの解放(メモリリーク防止)
    If Not rsTarget Is Nothing Then
        If rsTarget.State = adStateOpen Then rsTarget.Close
        Set rsTarget = Nothing
    End If
    Exit Sub

ErrorHandler:
    ' 7. ロールバック(エラー発生時は、それまでの処理をすべて破棄)
    If Not conn Is Nothing Then
        If conn.State = adStateOpen Then
            conn.RollbackTrans
            Debug.Print "Transaction Rolled Back due to Error: " & Err.Description
        End If
    End If
    
    ' エラー情報の通知
    MsgBox "Critical Error during import: " & Err.Description, vbCritical, "Import Failed"
    Resume CleanUp
End Sub

4. アーキテクトの視点:運用における3つの急所

このコードを現場に投入する際、君が意識すべき「プロの作法」が3つある。

① トランザクションの重み

上記のコードでは `conn.BeginTrans` を使用している。これは、1万件のデータのうち9,999件目がエラーになったとき、「中途半端にデータが入った状態」を防ぐためだ。
「データが重複している」「一部だけ入っている」といった不整合は、後続の業務プロセスを破壊する致命的なバグになる。「All or Nothing」。これがデータエンジニアの鉄則だ。

② パフォーマンスのトレードオフ

上記のコードでは、各レコードごとに `rsTarget.Fields(fldSrc.Name)` を評価している。これは非常に柔軟だが、数百万件規模のデータではオーバーヘッドになる。
もしパフォーマンスがボトルネックになることが予見されるなら、「インポート前にターゲットのフィールド名一覧を `Collection` や `Dictionary` オブジェクトに格納しておく」という最適化を施せ。

③ 型の不一致(Type Mismatch)への備え

`fldTgt.Value = fldSrc.Value` は、型が一致していれば完璧に動作する。しかし、ソースが「文字列の ‘123’」で、ターゲットが「数値型」の場合、ADOはエラーを吐く。
実務レベルのツールにするなら、フィールドの `Type` プロパティを比較し、必要に応じて `CInt` や `CDbl`、あるいは `Format` 関数による型変換を挟む「型変換レイヤー」を実装することを強く推奨する。

結びに代えて

「動かないコード」を書く者は、仕様書に従う。
「変化に強いコード」を書く者は、データ構造を理解する。

`Fields`コレクションを単なる値の入れ物としてではなく、「データのメタデータを解釈するための窓口」として捉えることができたとき、君の書くプログラムは、単なる自動化ツールから、真の「エンジンの設計」へと昇華する。

次なるプロジェクトでは、ハードコーディングの誘惑を断ち切り、この設計思想を実装してみせよ。

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