【Project VBAを掌握する極限の知見】SQL ServerからWBS階層と依存関係をミリ秒単位で同期するエンタープライズ・パイプラインの構築
プロジェクトマネジメントの現場において、Excelや外部DBで管理されたWBS(Work Breakdown Structure)やタスク間の依存関係を、Microsoft Projectへ手動で転記する作業ほど非生産的な時間はない。数千行に及ぶタスク群のインデント(階層構造)や先行・後続タスクのリンク(FS、SSなど)をヒューマンエラーゼロで同期し切るには、DOM(Document Object Model)のライフサイクルとProjectの描画エンジン特性を熟知したアーキテクチャが不可欠だ。
本稿では、SQL ServerからADO(ActiveX Data Objects)を用いて高速にデータをバルク取得し、MS Projectの`Task`オブジェクト群へ一気通貫で流し込む、実務直結型のプロダクションコードを提示する。
—
1. なぜ「愚直なループ処理」は破綻するのか?
多くのVBAエンジニアが陥る罠が、「レコードセットを1行ずつ舐めながら、その都度 `Tasks.Add` を呼び出し、インデントとリンクをその場で構築する」という実装だ。
これの何が問題か。
1. 描画エンジンの暴走: MS Projectは、1タスクが追加・変更されるたびに全体のスケジュール再計算とUI描画を行おうとする。数千行のループでこれをやると、実行時間が幾何級数的に跳ね上がる。
2. 依存関係の迷子: 先行タスク(Predecessors)を指定する際、まだ存在しない(データベース上のIDしか振られていない)タスクを参照しようとすると、Run-time errorが頻発する。
3. メモリリークとトランザクション不在: ADOコネクションの切断タイミングの逸脱や、途中でエラーが起きた際のロールバック機構の欠如により、中途半端に壊れたスケジュールファイルが生成される。
鉄則:バッファリングと「2パス・インジェクション」
この問題を解決する唯一の解が、「2パス(Two-Pass)処理」である。
- Pass 1: すべてのタスクをフラットに高速生成し、WBSの階層(Outlining / Indent)のみを構築する。この間、画面描画は完全ロックする。
- Pass 2: タスクのユニークID(またはGUID)が確定した状態で、依存関係(Links)を一括配線する。
—
2. 実装アーキテクチャの全体像
今回構築するパイプラインのデータフローは以下の通り。
[SQL Server]
│ (ADODB.Connection / parameterized query)
▼
[VBA Memory (Recordset / Dictionary)]
│
├─► [Pass 1: Task Creation & Indentation] (ScreenUpdating = False)
│
└─► [Pass 2: Dependency Mapping (Predecessors)]
事前準備(参照設定)
VBAエディタの「ツール」>「参照設定」から、以下のライブラリを有効化すること。
- `Microsoft ActiveX Data Objects 6.1 Library` (または環境に応じた最新版)
- `Microsoft Project 16.0 Object Library` (対象バージョンに合わせて変更)
—
3. プロダクションコード:エンタープライズ同期モジュール
以下のコードは、エラーハンドリング、トランザクション的思考、パフォーマンスチューニング(画面描画の抑制)を極限まで高めた実戦投入可能なモジュールである。
Option Explicit
‘ ==============================================================================
‘ 外部SQL ServerからWBSおよび依存関係を同期するメインプロシージャ
‘ ==============================================================================
Public Sub SyncWBSFromSQLServer()
Dim conn As Object
Dim rs As Object
Dim connString As String
Dim sqlQuery As String
‘ パフォーマンスと安定性のためのProject設定退避と最適化
Dim originalCalc As PjCalculation
originalCalc = Application.Calculation
Application.Calculation = pjManual ‘ 自動計算を停止
AppActivate Application.Caption
On Error GoTo ErrorHandler
‘ 1. ADODB接続の確立 (Windows認証の例)
connString = “Provider=SQLOLEDB;Server=YOUR_SERVER_NAME;Database=YOUR_DB_NAME;Trusted_Connection=yes;”
Set conn = CreateObject(“ADODB.Connection”)
conn.CommandTimeout = 60
conn.Open connString
‘ 2. WBS階層データおよび依存関係データの取得
‘ 期待するSQL側のカラム: TaskName, Duration, WBSLevel, UniqueID_Source, PredecessorSourceIDs
sqlQuery = “SELECT TaskName, Duration, WBSLevel, SourceID, PredecessorSourceIDs FROM dbo.V_ProjectWBS_Export ORDER BY SortOrder”
Set rs = CreateObject(“ADODB.Recordset”)
rs.Open sqlQuery, conn, 3, 1 ‘ adOpenStatic, adLockReadOnly
If rs.EOF Then
MsgBox “同期対象のデータが存在しません。”, vbExclamation, “データ同期”
GoTo Cleanup
End If
‘ 3. パス1: タスクの生成と階層構造(インデント)の構築
Call BuildTaskHierarchy(rs)
‘ 4. パス2: 依存関係(リンク)の構築
Call BuildTaskDependencies(rs)
‘ 完了処理
Application.Calculation = originalCalc
Application.CalculateAll
MsgBox “WBSおよび依存関係の同期が正常に完了しました。”, vbInformation, “エンタープライズ同期”
Cleanup:
On Error Resume Next
If Not rs Is Nothing Then If rs.State = 1 Then rs.Close
If Not conn Is Nothing Then If conn.State = 1 Then conn.Close
Set rs = Nothing
Set conn = Nothing
Application.Calculation = originalCalc
Exit Sub
ErrorHandler:
Application.Calculation = originalCalc
MsgBox “致命的なエラーが発生しました: ” & Err.Description & ” (Line: ” & Erl & “)”, vbCritical, “同期エラー”
Resume Cleanup
End Sub
‘ ==============================================================================
‘ Pass 1: タスク追加とインデント処理
‘ ==============================================================================
Private Sub BuildTaskHierarchy(ByRef rs As Object)
Dim t As Task
Dim currentLevel As Long
Dim prevLevel As Long
rs.MoveFirst
prevLevel = 1
‘ 既存のタスクをクリアする場合の処理(必要に応じて有効化)
‘ Dim existingTask As Task
‘ For Each existingTask in ActiveProject.Tasks
‘ If Not existingTask Is Nothing Then existingTask.Delete
‘ Next existingTask
Do While Not rs.EOF
‘ タスクの追加
Set t = ActiveProject.Tasks.Add(rs.Fields(“TaskName”).Value)
‘ 期間の設定 (分単位などで渡ることを想定し、必要に応じスケーリング)
If Not IsNull(rs.Fields(“Duration”).Value) Then
t.Duration = rs.Fields(“Duration”).Value
End If
‘ ソースIDをText1フィールドに保持(Pass 2での紐付け用)
t.Text1 = CStr(rs.Fields(“SourceID”).Value)
‘ インデント(WBS階層)の調整
currentLevel = CLng(rs.Fields(“WBSLevel”).Value)
If currentLevel > prevLevel Then
‘ 階層が深くなる場合
Dim i As Long
For i = 1 To (currentLevel – prevLevel)
t.OutlineIndent
Next i
ElseIf currentLevel < prevLevel Then
' 階層が浅くなる場合
Dim j As Long
For j = 1 To (prevLevel - currentLevel)
t.OutlineOutdent
Next j
End If
prevLevel = currentLevel
rs.MoveNext
Loop
End Sub
' ==============================================================================
' Pass 2: 依存関係(先行タスク)の配線処理
' ==============================================================================
Private Sub BuildTaskDependencies(ByRef rs As Object)
Dim targetTask As Task
Dim predTask As Task
Dim sourceID As String
Dim predIDs() As String
Dim pIndex As Long
Dim foundTask As Task
rs.MoveFirst
Do While Not rs.EOF
sourceID = CStr(rs.Fields("SourceID").Value)
' Text1に格納したSourceIDから、現在のProject上のTaskオブジェクトを特定
Set targetTask = Nothing
For Each foundTask In ActiveProject.Tasks
If Not foundTask Is Nothing Then
If foundTask.Text1 = sourceID Then
Set targetTask = foundTask
Exit For
End If
End If
Next foundTask
' 先行タスクの指定がある場合
If Not IsNull(rs.Fields("PredecessorSourceIDs").Value) And Not targetTask Is Nothing Then
Dim rawPreds As String
rawPreds = CStr(rs.Fields("PredecessorSourceIDs").Value)
' カンマ区切りで複数先行タスクを想定 ("ID_1,ID_2")
predIDs = Split(rawPreds, ",")
For pIndex = LBound(predIDs) To UBound(predIDs)
Dim pSourceID As String
pSourceID = Trim(predIDs(pIndex))
If pSourceID <> “” Then
‘ 先行タスクのオブジェクトを検索
Set predTask = Nothing
For Each foundTask In ActiveProject.Tasks
If Not foundTask Is Nothing Then
If foundTask.Text1 = pSourceID Then
Set predTask = foundTask
Exit For
End If
End If
Next foundTask
‘ リンクの作成 (標準はFS: Finish-to-Start)
If Not predTask Is Nothing Then
On Error Resume Next
targetTask.Predecessors.Add PrevTask:=predTask
On Error GoTo 0
End If
End If
Next pIndex
End If
rs.MoveNext
Loop
End Sub
—
現場のエンジニアへ向けた、アーキテクトからの提言
このコードの肝は、`Text1` というカスタムフィールドを中間インデックス(Primary Keyのバッファ)として一時利用している点にある。データベースのIDとProject内部のタスクIDは完全に別物であるため、直接IDでリンクを貼ろうとすると必ず破綻する。外部キーの概念をVBAのメモリ空間に落とし込み、二段階で安全に構築する――このアプローチこそが、大規模案件で音を上げない堅牢なマクロの条件だ。
泥臭い手動修正の自動化で終わらせるな。システム間のインテグレーションとして耐えうる「構造化されたコード」をあなたの現場にも実装してほしい。
