Project VBAを掌握する極限の知見:外部SQL ServerからのWBS階層・依存関係ハイパフォーマンス同期パイプライン
Microsoft ProjectのVBA開発において、最大にして唯一のタブーは「場当たり的なオブジェクト操作の繰り返し」である。数千行に及ぶWBS(Work Breakdown Structure)を外部データベースから取得し、階層構造(インデント)と複雑な先行・後続タスクの依存関係(Predecessors)を構築する際、通常のコードではメモリリーク、COMコンポーネントの肥大化、そして何より「再計算の嵐」による致命的なパフォーマンス低下を引き起こす。
本稿では、SQL ServerからADO(ActiveX Data Objects)を用いて一括取得したフラットデータを、MS Projectの内部エンジンに最適化された順序で流し込み、極限のパフォーマンスと堅牢性を両立させるエンタープライズレベルのデータ連携パイプラインの全貌を解説する。
—
1. アーキテクチャの要諦:なぜ通常のインポートでは破綻するのか
外部DBからタスクを同期する際、以下の3つの壁に直面する。
1. 暗黙の再計算(Calculation Engine Overhead):タスクを1件追加するたびにProjectが全体スケジュールを再計算するため、件数が増えるほどO(n^2)に近いオーダーで処理が重くなる。
2. 依存関係の不整合(Orphaned Dependencies):先行タスクがまだ作成されていない段階で依存関係を設定しようとすると、COMエラー(Run-time error)が発生する。
3. WBS階層の順序性(Hierarchy Dependency):親タスクが存在しない状態で子タスクをインデント(Outdent / Indent)することはできない。
これらを解決するための鉄則は、「一括取得・計算停止・トポロジカルソート(または適切な階層順ソート)・一括書き込み・最後の一括解放」である。
—
2. エンタープライズ・同期パイプラインの実装コード
以下のVBAモジュールは、SQL ServerからADO経由でWBSデータと依存関係を取得し、MS Projectへ高速かつ安全に同期するプロダクション品質のコードである。
Option Explicit
‘ =========================================================================
‘ 外部SQL Server WBS & 依存関係 同期エンジン
‘ アーキテクチャ設計: チーフアーキテクト
‘ =========================================================================
‘ Win32 API: 処理速度計測用(ミリ秒単位の高精度タイマー)
If VBA7 Then
Private Declare PtrSafe Sub Sleep Lib “kernel32” (ByVal dwMilliseconds As Long)
Else
Private Declare Sub Sleep Lib “kernel32” (ByVal dwMilliseconds As Long)
End If
Public Sub SynchronizeWBSFromSQLServer()
Dim conn As Object
Dim rsTasks As Object
Dim rsDeps As Object
Dim connString As String
Dim sqlTasks As String
Dim sqlDeps As String
‘ パフォーマンス最適化のための退避変数
Dim originalCalcMode As Long
On Error GoTo ErrorHandler
‘ 1. プロジェクトの自動計算を一時停止(極限の高速化の要)
originalCalcMode = Application.Calculation
Application.Calculation = pjCalculationManual
‘ 既存のタスクを全クリア(必要に応じてアップデート方式に変更可能)
‘ 注意: 本番環境ではUIDベースのUPSERTロジックを推奨
Dim t As Task
For Each t In ActiveProject.Tasks
If Not t Is Nothing Then t.Delete
Next t
‘ 2. ADODBコネクションの確立
Set conn = CreateObject(“ADODB.Connection”)
connString = “Provider=SQLOLEDB;Server=YOUR_SERVER_NAME;Database=YOUR_DB_NAME;Uid=YOUR_USER;Pwd=YOUR_PASSWORD;”
conn.CommandTimeout = 60
conn.Open connString
‘ 3. WBSタスクデータの取得(階層順にソートされていることが前提)
sqlTasks = “SELECT TaskID, TaskName, Duration, StartDate, OutlineLevel, WBSSequence ” & _
“FROM dbo.ProjectWBSData ORDER BY WBSSequence ASC;”
Set rsTasks = conn.Execute(sqlTasks)
‘ 4. タスクの一括生成と階層構造の構築
Dim newTask As Task
Dim targetOutlineLevel As Long
Dim currentLevel As Long
currentLevel = 1
Do While Not rsTasks.EOF
‘ タスクの追加
Set newTask = ActiveProject.Tasks.Add(rsTasks.Fields(“TaskName”).Value)
‘ 基本プロパティの設定
newTask.Duration = rsTasks.Fields(“Duration”).Value 480 ‘ 分単位(1日=480分想定)
‘ 外部システムのIDをText1フィールドに保持(依存関係マッピング用)
newTask.Text1 = CStr(rsTasks.Fields(“TaskID”).Value)
‘ 階層(OutlineLevel)の調整
targetOutlineLevel = CLng(rsTasks.Fields(“OutlineLevel”).Value)
Do While currentLevel < targetOutlineLevel
newTask.OutlineIndent
currentLevel = currentLevel + 1
Loop
Do While currentLevel > targetOutlineLevel
newTask.OutlineOutdent
currentLevel = currentLevel – 1
Loop
rsTasks.MoveNext
Loop
‘ 5. 依存関係(Predecessors)データの取得と紐付け
sqlDeps = “SELECT PredecessorTaskID, SuccessorTaskID, DependencyType, LagDays ” & _
“FROM dbo.ProjectDependencies;”
Set rsDeps = conn.Execute(sqlDeps)
Dim predID As String, succID As String
Dim depType As String, lagDays As Long
Dim predTask As Task, succTask As Task
Do While Not rsDeps.EOF
predID = CStr(rsDeps.Fields(“PredecessorTaskID”).Value)
succID = CStr(rsDeps.Fields(“SuccessorTaskID”).Value)
depType = rsDeps.Fields(“DependencyType”).Value ‘ 例: “FS”, “SS” など
lagDays = Nz(rsDeps.Fields(“LagDays”).Value, 0)
‘ Text1に保持した外部IDからProject上のタスクオブジェクトを特定
Set predTask = FindTaskByCustomID(predID)
Set succTask = FindTaskByCustomID(succID)
If Not predTask Is Nothing And Not succTask Is Nothing Then
‘ 依存関係の付与 (LinkTasksメソッドを使用)
‘ 引数: FromTask, ToTask, RelationshipType
succTask.TaskDependencies.Add Predecessor:=predTask, Type:=GetDependencyTypeEnum(depType), Lag:=lagDays & “d”
End If
Set predTask = Nothing
Set succTask = Nothing
rsDeps.MoveNext
Loop
CleanUp:
‘ 6. 設定の復元と明示的なメモリ解放(COMリークの完全阻止)
Application.Calculation = originalCalcMode
If Not rsDeps Is Nothing Then
If rsDeps.State = 1 Then rsDeps.Close
Set rsDeps = Nothing
End If
If Not rsTasks Is Nothing Then
If rsTasks.State = 1 Then rsTasks.Close
Set rsTasks = Nothing
End If
If Not conn Is Nothing Then
If conn.State = 1 Then conn.Close
Set conn = Nothing
End If
‘ 変更の全体再計算を実行
ActiveProject.Application.CalculateAll
MsgBox “WBSおよび依存関係の同期が正常に完了しました。”, vbInformation
Exit Sub
ErrorHandler:
MsgBox “致命的なエラーが発生しました: ” & Err.Description, vbCritical
Resume CleanUp
End Sub
‘ =========================================================================
‘ ヘルパー関数群
‘ =========================================================================
Private Function FindTaskByCustomID(ByVal customID As String) As Task
Dim t As Task
For Each t In ActiveProject.Tasks
If Not t Is Nothing Then
If t.Text1 = customID Then
Set FindTaskByCustomID = t
Exit Function
End If
End If
Next t
Set FindTaskByCustomID = Nothing
End Function
Private Function GetDependencyTypeEnum(ByVal typeStr As String) As Long
Select Case UCase(Trim(typeStr))
Case “FF”: GetDependencyTypeEnum = pjFinishToFinish
Case “SS”: GetDependencyTypeEnum = pjStartToStart
Case “SF”: GetDependencyTypeEnum = pjStartToFinish
Case Else: GetDependencyTypeEnum = pjFinishToStart ‘ デフォルトはFS
End Select
End Function
Private Function Nz(ByVal varVal As Variant, ByVal valIfNull As Variant) As Variant
If IsNull(varVal) Then Nz = valIfNull Else Nz = varVal
End Function
—
3. チーフアーキテクトが指摘する「死角」と最適化の極意
上記のコードベースをプロダクション環境に投入するにあたり、シニアエンジニアが押さえておくべき極限の知見を共有する。
A. O(N) 検索の罠とカスタムID索引のハッシュ化
上記の `FindTaskByCustomID` 関数は、タスク数 $N$ に対して線形探索(`For Each`)を行っているため、依存関係の紐付けフェーズで $O(N^2)$ の計算量となり、タスクが数千件を超えると急激に処理が破綻する。
極限のパフォーマンスを求める場合、インポート直後に `Scripting.Dictionary` オブジェクトをメモリ上に生成し、`Text1`(外部ID)をキー、`Task` オブジェクトをアイテムとして保持すべきである。これにより、検索コストを $O(1)$ まで圧縮できる。
‘ 高速化のためのDictionary活用イメージ
Dim dictTasks As Object
Set dictTasks = CreateObject(“Scripting.Dictionary”)
‘ タスク生成時に dictTasks.Add CStr(rsTasks.Fields(“TaskID”).Value), newTask を実行
B. カレンダー制約とスケーリングの呪縛
SQL Serverから取得した `StartDate` や `Duration` をそのまま代入すると、MS Project側の「カレンダー(稼働日・非稼働日)」および「タスク型(固定単位、固定期間など)」の暗黙の制約により、日付が勝手にずれる現象が発生する。
企業間連携においてスケジュールを厳密に一致させるためには、タスク生成後に明示的に `ConstraintType`(例:`pjConstraintStartNoEarlierThan`)を設定し、データベース側のタイムゾーンとProject側のベースカレンダーの整合性を完全に一致させるアーキテクチャ設計が不可欠である。
C. COMオブジェクトのライフサイクル管理
VBAにおける `CreateObject` や `ADODB.Connection` は、参照カウントの管理を誤るとExcelやProjectのプロセス内にメモリリークとして残留し、最終的にCOM例外を引き起こす。
本稿のコードのように、エラーハンドラ(`ErrorHandler`)を経由して必ず `Close` と `Set … = Nothing` を明示的に通す構造を徹底すること。中途半端なイテレーションの途中で `On Error Resume Next` を多用する悪習は、デバッグを不可能なものにするため厳禁である。
—
結び
Project VBAによる外部システム連携は、単なる「データの移送」ではない。MS Projectという極めて特殊な制約を持つスケジュールエンジンに対し、いかに矛盾のない順序でデータを流し込み、CPUとメモリの負荷をコントロールするかという「システム調律の芸術」である。
ここに示した知見が、レガシーとモダンが交錯する現場において、堅牢で美しい自動化パイプラインを構築する礎となることを確信している。
