【実務・中級編】【上級者向け】SQL Serverと連携したカスタムフィールドの自動マッピング – Project VBA解析バイブル

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

Project VBAを掌握する極限の知見:SQL Serverとの同期における「真の堅牢性」を求めて

MS Projectを単なる「スケジューリング・ツール」として使っているうちは、まだ初級者だ。
真のプロジェクト管理システムを構築するエンジニアにとって、Projectは「データの一部を可視化するためのフロントエンド」に過ぎない。真のデータ(Single Source of Truth)は、SQL Serverなどのリレーショナルデータベース(RDB)に存在すべきなのだ。

しかし、多くの開発者がここで躓く。SQL Serverからデータを取得し、Projectのカスタムフィールドに流し込む。一見単純なタスクに見えるが、その実装には「パフォーマンスの罠」と「データの不整合」という、実務を破綻させる二大リスクが潜んでいる。

本稿では、単なる「データのコピペ」ではない。大規模プロジェクトでも耐えうる、高効率かつ堅牢な「SQL Server ↔ MS Project 同期エンジン」の設計思想と実装コードを伝授する。

1. 凡庸なエンジニアが陥る「N+1問題」の罠

まず、私がレビューで真っ先に突き返す、典型的な「悪い設計」を提示しよう。

【アンチパターン】タスクごとにSQLを発行する設計

' --- 決してやってはいけない設計 ---
For Each t In ActiveProject.Tasks
    If Not t Is Nothing Then
        ' タスクごとにSQLを実行(これが致命的な遅延を生む)
        sql = "SELECT Cost FROM ProjectData WHERE TaskID = " & t.ID
        Set rs = conn.Execute(sql)
        If Not rs.EOF Then
            t.Text1 = rs!Cost ' カスタムフィールドへ書き込み
        End If
    End If
Next t

なぜこれが「ゴミ」なのか? 理由は明白だ。
「ネットワークI/Oのオーバーヘッド」だ。タスクが1,000個あれば、1,000回のSQL発行と1,000回のネットワーク往復が発生する。ProjectのCOMオブジェクト操作はそれ自体が重い。その上で、ループ内でDB接続を繰り返すなど、設計として極めて低レベルだ。

2. アーキテクチャの黄金律:メモリ内マッピング戦略

プロフェッショナルが採用すべき戦略は、「一括取得(Bulk Fetch)とメモリ内照合(In-Memory Mapping)」である。

設計の3原則

1. I/Oの最小化: SQL発行は、必要なデータを一括ですべて取得する1回(または数回)に限定する。
2. Dictionaryによる高速検索: 取得したデータは、VBAの `Scripting.Dictionary` を用いてメモリ上に展開する。これにより、タスクとの照合コストを $O(1)$ にまで抑え込む。
3. キーの厳格性: `TaskName`(タスク名)を結合キーにしてはならない。名前は重複し、変更される。必ず `UniqueID` をキーとして使用せよ。

3. プロダクション・コードの実装例

以下に、実務でそのまま耐えうる設計のコードを示す。このコードは、SQL Serverからデータを一括取得し、Projectの `UniqueID` をキーとしてカスタムフィールド(`Text1` を想定)へ高速にマッピングする。

Option Explicit

' ---------------------------------------------------------------------------
' Module: ProjectDataConnector
' Description: SQL Serverからデータを取得し、Projectのカスタムフィールドに
'              高速に同期するための高度なコネクタ。
' ---------------------------------------------------------------------------

' 外部参照設定: 
' 1. Microsoft ActiveX Data Objects 6.1 Library
' 2. Microsoft Scripting Runtime

Private Const CONN_STR As String = "Provider=SQLNCLI11;Server=YOUR_SERVER;Database=YOUR_DB;Trusted_Connection=yes;"

''' <summary>
''' プロジェクトのカスタムフィールドをSQL Serverの情報で一括更新する
''' </summary>
Public Sub SyncCustomFieldsWithSQL()
    Dim dictData As Scripting.Dictionary
    Dim task As Task
    Dim startTime As Double
    
    On Error GoTo ErrorHandler
    
    startTime = Timer
    Debug.Print "Sync Started: " & Now
    
    ' 1. SQL Serverからデータをメモリ上にロード
    ' ここでは Dictionary(UniqueID -> Value) の形式で保持する
    Set dictData = FetchDataFromSQL()
    
    If dictData Is Nothing Then
        MsgBox "データの取得に失敗しました。", vbCritical
        Exit Sub
    End If
    
    ' 2. Projectの計算モードを一時停止(パフォーマンス向上のため)
    ' ※ProjectにはExcelのようなCalculationモードの直接制御が限定的なため、
    ' 画面更新の停止と、必要に応じて再計算を制御する
    Application.DisplayAlerts = False
    
    ' 3. メモリ内照合による高速書き込み
    Dim updateCount As Long: updateCount = 0
    
    ' Projectのタスクを走査
    For Each task In ActiveProject.Tasks
        If Not task Is Nothing Then
            ' UniqueIDをキーとしてDictionaryを検索 (O(1)の計算量)
            If dictData.Exists(task.UniqueID) Then
                ' カスタムフィールド(Text1)へ値を設定
                ' ここではSQLから取得した値を格納
                task.Text1 = dictData(task.UniqueID)
                updateCount = updateCount + 1
            End If
        End If
    Next task
    
    ' 4. 完了通知
    Application.DisplayAlerts = True
    Debug.Print "Sync Completed: " & Now
    Debug.Print "Updated Tasks: " & updateCount
    Debug.Print "Execution Time: " & Format(Timer - startTime, "0.00") & " sec"
    
    MsgBox "同期が完了しました。" & vbCrLf & _
           "更新タスク数: " & updateCount & vbCrLf & _
           "実行時間: " & Format(Timer - startTime, "0.00") & " 秒", vbInformation

    Exit Sub

ErrorHandler:
    Application.DisplayAlerts = True
    MsgBox "致命的なエラーが発生しました。" & vbCrLf & _
           "Error No: " & Err.Number & vbCrLf & _
           "Description: " & Err.Description, vbCritical
End Sub

''' <summary>
''' SQL Serverからデータを一括取得し、Dictionaryに格納する
''' </summary>
''' <returns>Key: UniqueID (Long), Value: SyncValue (String)</returns>
Private Function FetchDataFromSQL() As Scripting.Dictionary
    Dim conn As ADODB.Connection
    Dim rs As ADODB.Recordset
    Dim dict As Scripting.Dictionary
    Dim sql As String
    
    Set dict = New Scripting.Dictionary
    Set conn = New ADODB.Connection
    
    ' SQL文: UniqueIDと同期したい値を一括で取得
    ' プロジェクトの「UniqueID」はDB側でも保持しておくことが運用の鉄則
    sql = "SELECT TaskUniqueID, TargetValue FROM ProjectSyncTable"
    
    On Error GoTo SQL_Error
    
    conn.Open CONN_STR
    Set rs = conn.Execute(sql)
    
    ' Recordsetを回してDictionaryに詰め込む
    ' これにより、ネットワークI/Oはここで完了する
    Do While Not rs.EOF
        ' UniqueIDをキー、値を値として格納
        ' Dictionaryのキーは重複を許さないため、一意性が保証される
        dict.Add CLng(rs!TaskUniqueID), CStr(rs!TargetValue)
        rs.MoveNext
    Loop
    
    Set FetchDataFromSQL = dict
    
    ' クリーンアップ
    rs.Close
    conn.Close
    Set rs = Nothing
    Set conn = Nothing
    Exit Function

SQL_Error:
    Debug.Print "SQL Error: " & Err.Description
    If Not rs Is Nothing Then If rs.State = adStateOpen Then rs.Close
    If Not conn Is Nothing Then If conn.State = adStateOpen Then conn.Close
    Set FetchDataFromSQL = Nothing
End Function

4. 設計の肝:なぜこのコードは「堅牢」なのか

このコードを単なる「動くコード」として捉えてはいけない。以下の設計判断が、プロダクション環境での安定性を担保している。

① `UniqueID` による不変性の確保

`Task.ID` は、タスクを削除したり並べ替えたりすると変わる可能性がある。一方、`UniqueID` はプロジェクトのライフサイクルを通じて不変だ。外部DBと連携する際の結合キー(Join Key)には、必ず `UniqueID` を選べ。これは設計における「絶対的な掟」だ。

② 空間計算量と時間計算量のトレードオフ

本実装では、`Scripting.Dictionary` を使用して、DBのデータをメモリ上に展開している。

  • 時間計算量: タスク数 $N$ に対して、DB接続は $O(1)$、ループは $O(N)$。合計 $O(N)$。
  • 空間計算量: メモリ上に $O(N)$ のデータ保持が必要。

数万件のタスクでない限り、現代のPCにおいてメモリ消費は無視できるほど小さく、ネットワークI/Oを削減するメリットが圧倒的に上回る。

③ エラーハンドリングの階層化

`SyncCustomFieldsWithSQL`(ビジネスロジック層)と `FetchDataFromSQL`(データアクセス層)を分離している。これにより、DB接続エラーが発生した際に、Projectのデータ操作(書き込み)に悪影響を及ぼす前に処理を安全に中断できる。

5. 次のステップへの助言

もし君がさらに上のレベルを目指すなら、以下の概念を検討せよ。

  • 差分更新(Delta Update): 全件更新ではなく、`LastModified` カラムを利用して、前回実行時以降に変更されたデータのみを取得する。
  • トランザクションの擬似実装: Project VBAにはDBのような強力なロールバック機能はない。更新前に現在の値をバックアップする、あるいは「更新中フラグ」をカスタムフィールドに持たせるなどの工夫が必要だ。

エンジニアリングとは、単に動くものを作ることではない。「変化に強く、予測可能なもの」を作ることだ。この設計思想を、君のプロジェクトに組み込んでほしい。

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