【上級者向け】外部SQL ServerからWBS階層と依存関係データを取得し、Projectへ同期するデータ連携パイプライン
プロフェッショナルな現場において、Microsoft Project(以下、MS Project)を単なる「お絵描きツール」として扱っているうちは、大規模プロジェクトの統制など到底おぼつかない。真に価値を発揮させるためには、基幹系データベース(SQL Server)で一元管理されているWBSマスターやリソース、先行・後続タスクの依存関係を、VBAを用いてミリ秒単位の整合性をもって同期させなければならない。
しかし、多くの開発者がこの「データベースからProjectへの同期」で挫折する。
原因は単純だ。MS Projectのオブジェクトモデル、特に「タスクの挿入順序」「先行タスク(Predecessors)のインデックス解決」「UIDとIDの混同」というライフサイクルの罠を理解していないからだ。
今回は、数千行規模のWBSを一瞬で構築し、複雑な依存関係をノーエラーで結びつける、エンタープライズ水準のデータ連携パイプラインの設計思想と実装コードを伝授する。
—
1. なぜ「力技のループ処理」では破綻するのか?
初心者がやりがちな最悪のアンチパターンは、SQL Serverからレコードを1件ずつ取得し、その都度 `ActiveProject.Tasks.Add` を呼び出し、さらにその場で `Task.Predecessors.Add` を実行する手法だ。
このアプローチがなぜ破綻するのか。理由は3つある。
1. パフォーマンスの絶望的な劣化:COMオブジェクトの往復(Marshaling)がレコード数分だけ発生し、数千件のタスクで数十分を要する。
2. 依存関係の参照切れ:後続タスクを追加している時点では、まだデータベース上の先行タスクがMS Project側に存在していない、あるいはIDが一致しないというインデックスのズレが発生する。
3. WBS階層の崩壊:親タスク(Summary Task)が作成される前に子タスクがインデントされると、アウトライン構造が完全に破壊される。
究極の解決策:一括データフェッチと「2パス・トポロジカル処理」
この問題を根本から解決する設計思想が、以下の2点だ。
- ADOによる高速一括フェッチ:レコードセットを一気にメモリ上に読み込み、二次元配列(Variant)として高速処理する。
- 2パス(2-Phase)構築:
- Pass 1:タスクのフラットな全件作成と、WBS階層(Indent / OutlineLevel)の構築。
- Pass 2:すべてのタスクが存在する状態で、先行・後続タスクの依存関係(Link)を安全に結びつける。
—
2. エンタープライズ・同期パイプラインの実装コード
以下のコードは、エラーハンドリング、トランザクション的な概念、そしてパフォーマンスを極限まで高めたプロダクションコードである。VBEの「参照設定」から `Microsoft ActiveX Data Objects 6.x Library` を有効化して使用してほしい。
Option Explicit
‘ =========================================================================
‘ 外部SQL ServerからWBS階層・依存関係を取得し、MS Projectへ同期するメインプロシージャ
‘ =========================================================================
Public Sub SyncWBSFromSQLServer()
Dim conn As Object
Dim rs As Object
Dim connStr As String
Dim sql As String
‘ 接続文字列(環境に合わせて変更してください)
connStr = “Provider=SQLOLEDB;Server=YOUR_SERVER_NAME;Database=YOUR_DB_NAME;Uid=YOUR_USER;Pwd=YOUR_PASSWORD;”
‘ SQLクエリ:WBS順、アウトライン階層、依存関係IDを網羅して取得
‘ TaskUID, TaskName, OutlineLevel, Duration, PredecessorUIDs を取得する前提
sql = “SELECT TaskUID, TaskName, OutlineLevel, Duration, PredecessorUIDs ” & _
“FROM dbo.VW_ProjectWBS_Export ” & _
“WHERE ProjectCode = ‘PRJ-2023-01’ ” & _
“ORDER BY SortOrder ASC;”
On Error GoTo ErrorHandler
‘ 画面描画と計算を停止し、圧倒的な高速化を実現
Application.ScreenUpdating = False
Application.Calculation = pjManual
‘ ADOオブジェクトの生成
Set conn = CreateObject(“ADODB.Connection”)
Set rs = CreateObject(“ADODB.Recordset”)
conn.Open connStr
rs.Open sql, conn, 1, 1 ‘ adOpenKeyset, adLockReadOnly
If rs.EOF Then
MsgBox “同期対象のデータが存在しません。”, vbExclamation, “データ連携エラー”
GoTo CleanUp
End If
‘ レコードをメモリ上の二次元配列へ一気に転送(COM往復の排除)
Dim vData As Variant
vData = rs.GetRows()
rs.Close
conn.Close
Dim totalRows As Long
totalRows = UBound(vData, 2) + 1
‘ ===================================================================
‘ Pass 1: タスクの作成とWBS階層(アウトライン)の構築
‘ ===================================================================
Dim i As Long
Dim t As Task
Dim currentLevel As Integer
Dim prevLevel As Integer
prevLevel = 1
For i = 0 to totalRows – 1
‘ vData(0, i): TaskUID
‘ vData(1, i): TaskName
‘ vData(2, i): OutlineLevel
‘ vData(3, i): Duration
Set t = ActiveProject.Tasks.Add(vData(1, i))
‘ カスタムフィールドなどに外部UIDを格納しておくことで後続処理のキーにする
t.Text1 = CStr(vData(0, i)) ‘ Text1にSQL側のTaskUIDを保持
t.Duration = vData(3, i)
currentLevel = CInt(vData(2, i))
‘ アウトラインレベルの調整
If currentLevel > prevLevel Then
‘ 階層が深くなる場合(インデント)
‘ ※Projectの仕様上、直前のタスクに対するインデントとなる
t.OutlineIndent
ElseIf currentLevel < prevLevel Then
' 階層が浅くなる場合(アウトデント)
Dim diff As Integer
Dim j As Integer
diff = prevLevel - currentLevel
For j = 1 To diff
t.OutlineOutdent
Next j
End If
prevLevel = currentLevel
Next i
' ===================================================================
' Pass 2: 依存関係(先行タスク)の結び付け
' ===================================================================
' 全タスクが生成されたため、UIDをキーにして安全にリンクを張る
Dim predUIDs As String
Dim predArray() As String
Dim p As Long
Dim targetTask As Task
Dim predecessorTask As Task
For i = 0 to totalRows - 1
predUIDs = CStr(vData(4, i)) ' カンマ区切りの先行タスクUID ("101,102")
If Len(predUIDs) > 0 Then
‘ 該当するProject上のタスクを取得(Text1に格納したUIDで検索)
Set targetTask = Nothing
On Error Resume Next
Set targetTask = FindTaskByUID(CStr(vData(0, i)))
On Error GoTo ErrorHandler
If Not targetTask Is Nothing Then
predArray = Split(predUIDs, “,”)
For p = LBound(predArray) To UBound(predArray)
Set predecessorTask = FindTaskByUID(Trim(predArray(p)))
If Not predecessorTask is Nothing Then
‘ 先行タスクを追加(デフォルトはFS関係)
targetTask.Predecessors.Add predecessorTask
End If
Next p
End If
End If
Next i
CleanUp:
‘ 設定を元に戻す
Application.ScreenUpdating = True
Application.Calculation = pjAutomatic
Application.CalculateAll
‘ オブジェクト解放
On Error Resume Next
If rs.State Then rs.Close
If conn.State Then conn.Close
Set rs = Nothing
Set conn = Nothing
MsgBox “SQL ServerからのWBS同期が正常に完了しました。”, vbInformation, “同期成功”
Exit Sub
ErrorHandler:
Application.ScreenUpdating = True
Application.Calculation = pjAutomatic
MsgBox “予期せぬエラーが発生しました。” & vbCrLf & _
“Error No: ” & Err.Number & vbCrLf & _
“Description: ” & Err.Description, vbCritical, “致命的エラー”
End Sub
‘ =========================================================================
‘ 補助関数: Text1に格納された外部UIDからMS ProjectのTaskオブジェクトを特定
‘ =========================================================================
Private Function FindTaskByUID(ByVal externalUID As String) As Task
Dim t As Task
For Each t In ActiveProject.Tasks
If Not t Is Nothing Then
If t.Text1 = externalUID Then
Set FindTaskByUID = t
Exit Function
End If
End If
Next t
Set FindTaskByUID = Nothing
End Function
—
3. チーフアーキテクトが指摘する「実務上の急所」
上記のコードを現場に導入する際、以下のポイントを押さえておかないと、運用フェーズで必ずシステムが悲鳴を上げる。
① `Text1` をキーにした仮想UIDマッピングの重要性
MS Projectにはネイティブで `UniqueID` というプロパティが存在するが、これはProjectが自動採番するものであり、外部SQL Server側の主キー(PK)とは一致しない。
外部キー制約をProject側で維持するためには、ユーザー定義テキストフィールド(例:`Text1` や `Number1`)に外部のIDを強制的に退避させるのが定石である。上記のコードで `FindTaskByUID` 関数が機能するのは、この設計を採用しているからに他ならない。
② 大規模データにおけるパフォーマンスのボトルネック
`FindTaskByuid` 内の `For Each t In ActiveProject.Tasks` は、タスク数が5,000件を超えると線形探索(O(N))のためPass 2の処理でパフォーマンスが低下する。
もし数万件規模のエンタープライズWBSを扱う場合は、あらかじめ `Scripting.Dictionary` オブジェクトをメモリ上に生成し、`[ExternalUID] -> [Task Object]` のハッシュマップを構築しておくべきだ。これにより、検索コストをO(1)へ劇的に圧縮できる。
③ トランザクションとロールバックの概念
データベースと異なり、MS ProjectのVBA操作は「一括ロールバック」が標準では効かない。Pass 1の途中でエラーが発生した場合、中途半端にタスクが生成されたプロジェクトファイルが残されてしまう。
実務でこのツールを組み込む際は、処理の冒頭で `ActiveProject.Save` を実行してバックアップポイントを作るか、失敗時に新規プロジェクトを破棄して元のファイルを再読み込みするなどのセーフティネットを必ず実装すること。
—
総括
VBAによる外部DB連携は、単なる「おまけのスクリプト」ではない。企業のプロジェクトマネジメント基盤の信頼性を左右する、極めてスパルタンで重要なパイプラインである。
「なぜ動かないのか」と悩む前に、MS Projectのオブジェクトが持つライフサイクル(いつ生成され、いつインデックスが確定するのか)をロジカルに想像しろ。その視点を持てた瞬間から、あなたの書くVBAコードは、ただの「自動化マクロ」から「堅牢なエンタープライズ・アーキテクチャ」へと昇華する。
