【テクニカル・上級編】【上級者向け】WBSの階層構造をJSON形式にシリアライズし、Webダッシュボードと連携するAPI開発 – Project VBA解析バイブル

スポンサーリンク

【上級者/チーフアーキテクトの知見】MS ProjectのWBSを極限まで高速シリアライズし、Webダッシュボードへ直結させるAPI設計

Microsoft Project(以下、MSP)のVBA開発において、最大のタブーは何であるか。それは、「タスクオブジェクトを安易にループさせ、セルやプロパティを都度参照すること」に他ならない。

数千行に及ぶWBS(Work Breakdown Structure)を抱える大規模プロジェクトにおいて、DOM(Document Object Model)的なタスク走査をVBAのガベージコレクタやCOM境界を意識せずに行えば、Excelであれば数秒で終わる処理が、MSPでは数分、あるいはメモリリークによるフリーズを引き起こす。

今回は、MSPが持つタスクツリーの階層構造(WBS)、前提条件(Predecessors)、依存関係(Successors)のメタデータを完璧に保持したまま、極限までメモリを最適化し、ネイティブなJSON文字列としてシリアライズしてWebフロントエンド(ガントチャート)へ直結させるAPIアーキテクチャを解説する。

—

1. アーキテクチャの核心:なぜ「標準のJSONライブラリ」では破綻するのか

VBAからWebダッシュボード(ReactやVue.jsなどを用いたモダンなガントチャートUI)へデータを渡す際、多くの初学者は「VBAでオブジェクトを舐めて、文字列結合でJSONを組み立てる」という愚行を犯す。

しかし、シニアエンジニアが直面する現実の課題は以下のような点にある。

1. COM境界を跨ぐコスト:
MSPの `Task.OutlineLevel` や `Task.Dependencies` をループ内で個別に呼び出すと、COM(Component Object Model)のプロセス境界を往復するオーバーヘッドにより、実行時間が幾何級数的に増大する。
2. 階層構造(WBS)の再帰的破綻:
WBSのインデントレベルやアウトライン番号(`WBS` または `OutlineNumber`)は、タスクの削除・追加で容易にズレが生じる。これを安全に親子関係(Tree構造)のJSON配列へ変換するには、フラットな配列をメモリ上で一気に構築し、スタック構造を模したポインタ操作を行う必要がある。
3. メモリ最適化と解放の鉄則:
MSPのVBAは、参照を明示的に `Nothing` に解放しないと、COMオブジェクトの参照カウントが残存し、ExcelやMSPプロセスがバックグラウンドにゾンビとして居座り続ける。

これを解決するため、本稿では「バッファリング配列への一括読み込み」「ポインタ/インデックスによる親子関係の紐付け」「StringBuilderパターンの自作による高速JSONシリアライズ」を実装する。

—

2. 実装:高速WBSシリアライザ(VBAコード)

以下のコードは、MSPのタスク群を一網打尽にメモリ(Variant配列)へ取り込み、COMの往復を最小限に抑えた上で、依存関係を含むJSON文字列を生成する実用モジュールである。

‘ ==============================================================================
‘ Module: MdlWbsJsonSerializer
‘ Author: Chief Architect
‘ Description: MS ProjectのWBS階層・依存関係を極限のパフォーマンスでJSON化する
‘ ==============================================================================
Option Explicit

‘ Win32 API for High-Resolution Timing (パフォーマンス計測用)
If VBA7 Then
Private Declare PtrSafe Function QueryPerformanceCounter Lib “kernel32” (ByRef lpPerformanceCount As Currency) As Long
Private Declare PtrSafe Function QueryPerformanceFrequency Lib “kernel32” (ByRef lpFrequency As Currency) As Long
Else
Private Declare Function QueryPerformanceCounter Lib “kernel32” (ByRef lpPerformanceCount As Currency) As Long
Private Declare Function QueryPerformanceFrequency Lib “kernel32” (ByRef lpFrequency As Currency) As Long
End If

Public Sub ExportWbsToJson(ByVal targetFilePath As String)
Dim startTime As Currency, freq As Currency
QueryPerformanceFrequency freq
QueryPerformanceCounter startTime

Dim t As Task
Dim tasksCount As Long

‘ 1. タスク数の取得と、存在チェック
On Error Resume Next
tasksCount = ActiveProject.Tasks.Count
On Error GoTo 0

If tasksCount = 0 Then
MsgBox “エクスポート対象のタスクが存在しません。”, vbExclamation
Exit Sub
End If

‘ 2. メモリ最適化:COMの往復を排除するため、必要なプロパティを一次元/二次元配列へキャッシュ
‘ 構造: 1=ID, 2=UniqueID, 3=Name, 4=Start, 5=Finish, 6=OutlineLevel, 7=WBS, 8=PercentComplete, 9=Predecessors
Dim taskData() As Variant
ReDim taskData(1 To tasksCount, 1 To 9)

Dim i As Long
i = 1

‘ 画面描画を停止し、処理速度を限界まで引き上げる
Application.ScreenUpdating = False

For Each t In ActiveProject.Tasks
If Not t Is Nothing Then
taskData(i, 1) = t.ID
taskData(i, 2) = t.UniqueID
taskData(i, 3) = EscapeJsonString(t.Name)
taskData(i, 4) = Format$(t.Start, “yyyy-mm-ddTHH:nn:ss”)
taskData(i, 5) = Format$(t.Finish, “yyyy-mm-ddTHH:nn:ss”)
taskData(i, 6) = t.OutlineLevel
taskData(i, 7) = t.WBS
taskData(i, 8) = t.PercentComplete
taskData(i, 9) = GetPredecessorsString(t)
i = i + 1
End If
‘ オブジェクトの明示的解放(リーク防止)
Set t = Nothing
Next t

‘ 3. 高速JSON文字列の構築 (StringBuilderパターンの模倣)
Dim jsonBuilder As String
jsonBuilder = “[” & vbCrLf

Dim row As Long
For row = 1 To tasksCount
If taskData(row, 1) <> “” Then
jsonBuilder = jsonBuilder & ” {” & vbCrLf
jsonBuilder = jsonBuilder & ” “”id””: ” & taskData(row, 1) & “,” & vbCrLf
jsonBuilder = jsonBuilder & ” “”uniqueId””: ” & taskData(row, 2) & “,” & vbCrLf
jsonBuilder = jsonBuilder & ” “”name””: “”” & taskData(row, 3) & “””,” & vbCrLf
jsonBuilder = jsonBuilder & ” “”start””: “”” & taskData(row, 4) & “””,” & vbCrLf
jsonBuilder = jsonBuilder & ” “”finish””: “”” & taskData(row, 5) & “””,” & vbCrLf
jsonBuilder = jsonBuilder & ” “”outlineLevel””: ” & taskData(row, 6) & “,” & vbCrLf
jsonBuilder = jsonBuilder & ” “”wbs””: “”” & taskData(row, 7) & “””,” & vbCrLf
jsonBuilder = jsonBuilder & ” “”percentComplete””: ” & taskData(row, 8) & “,” & vbCrLf
jsonBuilder = jsonBuilder & ” “”predecessors””: “”” & taskData(row, 9) & “””” & vbCrLf

If row < tasksCount Then jsonBuilder = jsonBuilder & " }," & vbCrLf Else jsonBuilder = jsonBuilder & " }" & vbCrLf End If End If Next row jsonBuilder = jsonBuilder & "]" ' 4. ファイル出力 (UTF-8 without BOM または UTF-8) SaveTextToFile jsonBuilder, targetFilePath Application.ScreenUpdating = True Dim endTime As Currency QueryPerformanceCounter endTime Debug.Print "WBS JSON Export Completed in: " & Format$((endTime - startTime) / freq, "0.000") & " seconds." MsgBox "WBSのJSONシリアライズが完了しました。", vbInformation End Sub ' 依存関係(Predecessors)の抽出 Private Function GetPredecessorsString(ByVal t As Task) As String Dim dep As TaskDependency Dim preds As String preds = "" On Error Resume Next For Each dep In t.Predecessors If preds <> “” Then preds = preds & “,”
preds = preds & dep.From.UniqueID
Set dep = Nothing
Next dep
On Error GoTo 0

GetPredecessorsString = preds
End Function

‘ JSONインジェクションを防ぐための最低限のエスケープ処理
Private Function EscapeJsonString(ByVal str As String) As String
str = Replace(str, “\”, “\\”)
str = Replace(str, “”””, “\”””)
str = Replace(str, vbCrLf, ” “)
str = Replace(str, vbCr, ” “)
str = Replace(str, vbLf, ” “)
EscapeJsonString = str
End Function

‘ ADODB.Streamを用いた堅牢なUTF-8ファイル出力
Private Sub SaveTextToFile(ByVal text As String, ByVal filePath As String)
Dim stream As Object
Set stream = CreateObject(“ADODB.Stream”)

With stream
.Type = 2 ‘ adTypeText
.Charset = “UTF-8”
.Open
.WriteText text
.SaveToFile filePath, 2 ‘ adSaveCreateOverWrite
.Close
End With
Set stream = Nothing
End Sub

—

3. Webダッシュボード(APIサーバー)との連携設計

上記VBAによって生成されたJSONファイルは、社内のローカルストレージや共有サーバーを経由して、Webダッシュボード側へ非同期に連携される。さらに高度なシステム間連携を目指す場合、VBA側から直接REST API(HTTP POST)を叩く設計も有効だ。

Webフロントエンド(JavaScript / D3.jsやdhtmlxGantt等)側での解釈の極意

VBAから出力されたフラットな配列(`outlineLevel` と `wbs` を持つデータ)を、フロントエンド側でツリー構造(Parent-Child)に変換するアルゴリズムの断片を以下に示す。このアプローチにより、VBA側に無駄な再帰処理を持たせず、計算量をサーバー/クライアントサイドにオフロードできる。

// フロントエンドでのWBSフラット配列からツリー構造へのO(N)変換アルゴリズム
function buildWbsTree(flatData) {
let map = {};
let roots = [];

// 1. マップの初期化
flatData.forEach(item => {
map[item.id] = { …item, children: [] };
});

// 2. 階層の構築 (OutlineLevelを利用したスタック的アプローチ)
let stack = [];

flatData.forEach(item => {
let node = map[item.id];

while (stack.length > 0 && stack[stack.length – 1].outlineLevel >= node.outlineLevel) {
stack.pop();
}

if (stack.length === 0) {
roots.push(node);
} else {
let parent = stack[stack.length – 1];
parent.children.push(node);
}

stack.push(node);
});

return roots;
}

—

4. チーフアーキテクトからの警鐘:レガシー環境運用における注意点

1. セキュリティゾーンとマクロの有効性:
ネットワーク共有フォルダ上の `.mpp` ファイルからマクロを実行する場合、Officeのセキュリティポリシー(MOTW: Mark of the Web)により実行が阻害される。API連携基盤を構築する際は、信頼できる場所(Trusted Locations)への配置をデプロイ手順に組み込むこと。
2. メモリリークの温床となるCOMオブジェクト:
前述のコードの通り、`For Each` ループ内で取得するオブジェクト(`Task`, `TaskDependency`)は、必ずループの最後で `Set xxx = Nothing` を明示しなければならない。これを怠ると、数回のエクスポート実行でMSPプロセスのメモリ消費量が数GBに膨れ上がり、OS全体のパフォーマンス低下を招く。
3. 文字コードの罠:
VBA標準の `Open … For Output` を使うとシフトJIS(CP932)で出力され、モダンなWebフロントエンド側(UTF-8前提)で文字化けを引き起こす。必ず `ADODB.Stream` を使用してUTF-8(BOM有無は要件に合せる)で吐き出すことが、プロフェッショナルとしての最低限の作法である。

—

結び

VBAは「時代遅れの言語」などではない。適切なメモリ管理、COMの挙動理解、そしてモダンなWebアーキテクチャとのインターフェース設計を施せば、レガシーなMicrosoft Projectを「超高速なエンタープライズWBSエンジン」へと変貌させることが可能だ。

現場の制約に嘆くのではなく、アーキテクチャの力で限界を突破せよ。

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