【実務・中級編】DAOとADOのハイブリッド設計:ローカル一時テーブルを活用したデータ加工戦略 – Access VBA解析バイブル

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

DAOとADOのハイブリッド設計:ローカル一時テーブルを活用した「極限のデータ加工戦略」

「動けばいい」という思考停止が、Accessアプリケーションの寿命を縮める。

現場のリーダーとして多くのプロジェクトを見てきたが、パフォーマンス不足に陥る開発者の共通点は、「DAOとADOの使い分け」と「データの移動コスト」を軽視していることだ。

外部SQL Serverなどの基幹DBから大量のデータを取得する際、ADOのRecordsetをそのまま回して、一行ずつ計算や加工を行い、またADOで更新をかける……。もし君がそんな実装を考えているなら、今すぐその設計書を破り捨ててほしい。それはネットワーク・レイテンシ(遅延)の泥沼に自ら飛び込むようなものだ。

本稿では、プロフェッショナルが実践する「DAOによるローカル一時テーブル活用型ハイブリッド設計」を伝授する。これは、データの「抽出(Extract)」「加工(Transform)」「格納(Load)」を、Accessのエンジン特性を最大限に活かして分離する、極めて堅牢かつ高速なアーキテクチャだ。

1. なぜ「ハイブリッド」でなければならないのか

まず、我々が直面している技術的な制約を整理しよう。

ADOの限界:ネットワーク・チャッティネス(Chattiness)

ADOは外部データベース(SQL Server, Oracle等)との通信において極めて強力だ。しかし、ネットワーク越しに `MoveNext` を繰り返しながら複雑なロジックを実行すると、1レコードごとにパケットのやり取りが発生し、通信回数が増大する(これを「Chattyな通信」と呼ぶ)。これが、処理が遅い最大の原因だ。

DAOの強み:ローカルエンジンの支配者

一方で、Accessのローカルエンジン(ACE/Jet)を操作するDAOは、ローカルファイルに対する操作において圧倒的なパフォーマンスを誇る。インデックスの効かせ方、メモリ管理、そしてローカルでの複雑な集計処理において、DAOは「ホームグラウンド」の利を最大限に享受できる。

結論としてのハイブリッド戦略

我々が取るべき戦略はこうだ。
1. ADO を使い、外部DBからデータを「バルク(一括)」で持ってくる。
2. 持ってきたデータを、Access内のローカル一時テーブル(DAO)へ一気に流し込む。
3. DAO を使い、ローカル環境の高速なインデックスとメモリを活用して、複雑な計算・加工を行う。
4. 加工済みの結果を、再び ADO を用いて外部DBへ一括で書き戻す。

この「データの局所化」こそが、大規模データ処理における正解である。

2. 実装アーキテクチャ:堅牢なETLプロセス

以下に、実務レベルでそのまま使える、モジュール化された設計例を示す。ここでは、一時テーブルの作成から、トランザクション管理、エラーハンドリングまでを網羅している。

プロダクション・コード・テンプレート

Option Compare Database
Option Explicit

' ======================================================================================
' Module: modDataProcessor
' Description: DAO/ADOハイブリッドによる高速データ処理エンジン
' Author: Chief Architect
' ======================================================================================

Public Sub ExecuteHighSpeedProcess()
    Dim db As DAO.Database
    Dim adoSrvConn As ADODB.Connection
    Dim strTempTableName As String
    
    ' 設定:一時テーブル名(既存の場合は削除してから使用)
    strTempTableName = "tmp_WorkData"
    
    On Error GoTo ErrorHandler
    
    Set db = CurrentDb
    
    ' 1. 前準備:古い一時テーブルのクリーンアップ
    PrepareLocalTable db, strTempTableName
    
    ' 2. データの抽出 (Extract) & ローカルへの取り込み (Load Part 1)
    ' 外部DBからADO経由で取得し、DAO経由でローカルへ一気に流し込む
    Debug.Print "Step 1: Extracting data from Remote Server..."
    Call ExtractRemoteToLocal(db, strTempTableName)
    
    ' 3. ローカルでのデータ加工 (Transform)
    ' DAOの高速なインデックスを利用して、複雑なロジックを実行
    Debug.Print "Step 2: Transforming data locally..."
    Call TransformLocalData(db, strTempTableName)
    
    ' 4. 外部DBへの書き戻し (Load Part 2)
    ' 加工済みのデータをADOで一括更新
    Debug.Print "Step 3: Syncing processed data to Remote Server..."
    Call SyncLocalToRemote(db, strTempTableName)
    
    ' 5. 後処理
    CleanupLocalTable db, strTempTableName
    
    MsgBox "プロセスが正常に完了しました。", vbInformation
    
ExitHandler:
    ' オブジェクトの解放(ライフサイクル管理の鉄則)
    On Error Resume Next
    If Not adoSrvConn Is Nothing Then
        If adoSrvConn.State = adStateOpen Then adoSrvConn.Close
    End If
    Set adoSrvConn = Nothing
    Set db = Nothing
    Exit Sub

ErrorHandler:
    MsgBox "致命的なエラーが発生しました: " & Err.Description, vbCritical, "Error " & Err.Number
    Resume ExitHandler
End Sub

' --------------------------------------------------------------------------------------
' 外部DBからデータを抽出し、ローカルのDAO Recordsetへ一括投入する
' --------------------------------------------------------------------------------------
Private Sub ExtractRemoteToLocal(ByRef db As DAO.Database, ByVal tableName As String)
    Dim adoSrvConn As New ADODB.Connection
    Dim adoSrvRs As New ADODB.Recordset
    Dim daoRs As DAO.Recordset
    Dim strConn As String
    Dim strSQL As String
    
    ' 接続文字列(環境に合わせて変更すること)
    strConn = "Provider=SQLOLEDB;Data Source=YourServerName;Initial Catalog=YourDB;Integrated Security=SSPI;"
    strSQL = "SELECT ID, Amount, Category FROM SalesData WHERE Processed = 0"
    
    adoSrvConn.Open strConn
    adoSrvRs.Open strSQL, adoSrvConn, adOpenStatic, adLockReadOnly
    
    ' ローカルテーブルへ追加するためのDAO Recordsetを開く
    Set daoRs = db.OpenRecordset(tableName, dbOpenTable)
    
    ' 重要:ADOのRecordsetをループするのではなく、
    ' 大量データの場合は、一括で行を追加するロジックを検討する
    ' ここでは理解を助けるため標準的なループを示すが、極限まで速くするには
    ' Bulk Insert的な手法(外部SQLの実行)を検討すべきだ
    Do Until adoSrvRs.EOF
        daoRs.AddNew
        daoRs!ID = adoSrvRs!ID
        daoRs!Amount = adoSrvRs!Amount
        daoRs!Category = adoSrvRs!Category
        daoRs.Update
        adoSrvRs.MoveNext
    Loop
    
    daoRs.Close
    adoSrvRs.Close
    adoSrvConn.Close
End Sub

' --------------------------------------------------------------------------------------
' ローカルのDAO Recordsetに対して、インデックスを活用した高速加工を行う
' --------------------------------------------------------------------------------------
Private Sub TransformLocalData(ByRef db As DAO.Database, ByVal tableName As String)
    Dim daoRs As DAO.Recordset
    
    ' ローカルテーブルにインデックスが貼られていることを前提とする
    ' インデックスがないと、加工フェーズで指数関数的に時間がかかる
    Set daoRs = db.OpenRecordset(tableName, dbOpenDynaset)
    
    ' トランザクションの開始(ローカルでの整合性担保)
    DBEngine.BeginTrans
    
    On Error GoTo InnerErrorHandler
    
    Do Until daoRs.EOF
        ' --- ここに複雑なビジネスロジックを記述 ---
        ' 例: カテゴリに基づいた重み付け計算
        If daoRs!Category = "Premium" Then
            daoRs!Amount = daoRs!Amount * 1.1
        End If
        ' ---------------------------------------
        
        daoRs.Update
        daoRs.MoveNext
    Loop
    
    DBEngine.CommitTrans
    daoRs.Close
    Exit Sub

InnerErrorHandler:
    DBEngine.Rollback
    Err.Raise Err.Number, "TransformLocalData", Err.Description
End Sub

' --------------------------------------------------------------------------------------
' 加工済みのデータをADOを使用して外部DBへ書き戻す
' --------------------------------------------------------------------------------------
Private Sub SyncLocalToRemote(ByRef db As DAO.Database, ByVal tableName As String)
    Dim adoSrvConn As New ADODB.Connection
    Dim daoRs As DAO.Recordset
    Dim strConn As String
    
    strConn = "Provider=SQLOLEDB;Data Source=YourServerName;Initial Catalog=YourDB;Integrated Security=SSPI;"
    adoSrvConn.Open strConn
    
    ' 外部DB側での整合性を保つため、ADOのトランザクションを利用
    adoSrvConn.BeginTrans
    
    Set daoRs = db.OpenRecordset(tableName, dbOpenSnapshot)
    
    On Error GoTo InnerErrorHandler
    
    Do Until daoRs.EOF
        ' ここでは、UPDATE文をADO Commandとして発行するのが最も堅牢だ
        ' 単純なRecordset.Updateではなく、SQLを発行することでネットワーク負荷を制御する
        Dim strUpdateSQL As String
        strUpdateSQL = "UPDATE SalesData SET Amount = " & daoRs!Amount & _
                       " WHERE ID = " & daoRs!ID
        
        adoSrvConn.Execute strUpdateSQL
        daoRs.MoveNext
    Loop
    
    adoSrvConn.CommitTrans
    daoRs.Close
    adoSrvConn.Close
    Exit Sub

InnerErrorHandler:
    adoSrvConn.Rollback
    Err.Raise Err.Number, "SyncLocalToRemote", Err.Description
End Sub

' --------------------------------------------------------------------------------------
' ユーティリティ:一時テーブルの管理
' --------------------------------------------------------------------------------------
Private Sub PrepareLocalTable(ByRef db As DAO.Database, ByVal tableName As String)
    On Error Resume Next
    db.Execute "DROP TABLE " & tableName
    On Error GoTo 0
    
    ' テーブル構造の作成(本番環境では事前に定義したテーブルをコピーする手法が望ましい)
    db.Execute "CREATE TABLE " & tableName & " (" & _
               "ID LONG PRIMARY KEY, " & _
               "Amount DOUBLE, " & _
               "Category TEXT(50));"
End Sub

Private Sub CleanupLocalTable(ByRef db As DAO.Database, ByVal tableName As String)
    On Error Resume Next
    db.Execute "DROP TABLE " & tableName
End Sub

3. アーキテクトが教える「バグを生まないための鉄則」

このコードをそのまま使うだけでは不十分だ。プロフェッショナルとして、以下の3点に魂を込めろ。

① オブジェクトのライフサイクルを厳格に管理せよ

VBAにおいて、`Recordset` や `Connection` オブジェクトは、明示的に `.Close` し、`Set = Nothing` で解放しなければならない。特に、エラーが発生した際にこれらのオブジェクトがメモリ上に「浮遊」し続けると、ファイルロックが発生したり、次回の実行時に「既に開かれています」といった不可解なエラーを招く。`ErrorHandler` 内での解放処理は、コードの美しさではなく、システムの生存戦略だ。

② トランザクションの境界線を明確にせよ

本稿のコードでは、「ローカルの加工(DAO)」と「外部への書き戻し(ADO)」で、それぞれ独立したトランザクションを張っている。
なぜ一つの巨大なトランザクションにしないのか? それは、外部DBへの書き戻しに時間がかかる間、ローカルのロックを保持し続けると、他のユーザーの操作を妨げるリスクがあるからだ。「失敗してもやり直せる最小単位」でトランザクションを設計すること。これが、分散システムにおける鉄則だ。

③ インデックスは「加工の武器」である

一時テーブル(`tmp_WorkData`)を作成する際、`CREATE TABLE` 文で `PRIMARY KEY` を指定していることに注目してほしい。
DAOによる加工フェーズで、もし `Category` による検索や結合を行うなら、必ず `CREATE INDEX` を実行してインデックスを貼っておけ。インデックスのないテーブルに対する `Do Until…MoveNext` は、データの増大とともに指数関数的にパフォーマンスを悪化させる。

結びに代えて

DAOとADOを使い分けることは、単なるテクニックではない。それは、「データの物理的な性質」を理解し、コンピューティングリソースを最適に配置する設計思想そのものだ。

「とりあえずADOでRecordsetを開いてループさせる」という安易な実装を卒業し、このハイブリッド設計を取り入れろ。君の作るツールは、劇的に速くなり、劇的に堅牢になるはずだ。

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