【実務・中級編】外部データベース(SQL Server)からタスク情報を取得し、Projectへ同期する連携手法 – Project VBA解析バイブル

スポンサーリンク

Project VBAを掌握する極限の知見:SQL Serverと同期するエンタープライズWBS自動構築エンジニアリング

開発現場でこんな絶望を味わったことはないだろうか。
「基幹システムやSQL Serverで管理されている最新のプロジェクト計画(WBS・依存関係)を、なぜ手作業でMS Projectに転記し直さなければならないのか」

数百行、数千行に及ぶタスクの親子関係(インデント)、複雑な先行・後続タスクのリンク(FS, SSなど)。これらを手動で同期させるアプローチは、ヒューマンエラーの温床であり、エンジニアリングの敗北だ。

今回は、ADODBを用いてSQL Serverから一撃でタスク群を取得し、MS ProjectのWBS階層・前提条件を完全自動構築するエンタープライズ向けVBAアーキテクチャを伝授する。

そこらへんの「とりあえず動くコード」とは一線を画す。オブジェクトのライフサイクル、Project特有の「遅延再計算の罠」、そして数千件を秒速で処理するためのメモリ最適化まで踏み込んだ、プロフェッショナルだけの知見を公開しよう。

1. なぜ「愚直なタスク追加」は破綻するのか?

多くの初学者が陥る罠が、レコードセットを上から順に舐めながら `Tasks.Add` を連発し、その場で依存関係(Predecessors)を設定していく手法だ。

これには2つの致命的な欠陥がある。

1. O(N^2)のパフォーマンス劣化と再計算の嵐
MS Projectはタスクが1件追加されるたびに、全体のスケジュール再計算をバックグラウンドで走らせようとする。数千件のタスクをこの方法で突っ込むと、プログレスバーが途中でフリーズしたかのように重くなる。
2. 依存先タスク未存在エラー
タスクBの前提条件としてタスクAを指定したいとき、まだタスクAがProject上に生成されていない(あるいはUniqueIDが確定していない)状態でリンクを張ろうとすると、容赦なく実行時エラー「1100」が飛んでくる。

解決の要諦

  • バッチ処理の発想: 既存タスクを一度すべてクリア(または差分更新)し、タスクの「作成」と「リンク(依存関係)」のフェーズを完全に分離する。
  • UniqueIDの活用: 名称ではなく、一意に定まるID(DB側のタスクIDとProjectのUniqueIDのマッピング)で関係性を制御する。

2. データベース設計の前提

SQL Server側には、以下のようなフラットかつ階層を表現できるテーブルが存在すると仮定する。

CREATE TABLE dbo.ProjectTasks (
TaskUID INT IDENTITY(1,1) PRIMARY KEY,
ProjectCode VARCHAR(50),
WBSLevel INT, — 階層深度(0がルート、1がサマリー…)
TaskName NVARCHAR(255),
DurationDays INT,
PredecessorUIDs VARCHAR(100) — カンマ区切りの先行タスクDB側のID (例: “12,15”)
);

この構造をVBA側でスマートに解釈し、MS Projectの強力なオブジェクトモデルへ流し込む。

3. プロダクションコード:SQL Server連携WBS自動構築エンジン

以下のコードは、エラーハンドリング、トランザクション的思考、そしてProject VBA特有のパフォーマンスチューニングを網羅した、現場でそのまま使える実戦的モジュールである。

Option Explicit

‘ =========================================================================
‘ 外部DB(SQL Server)からタスク情報を取得し、MS Projectを完全同期するモジュール
‘ =========================================================================
Public Sub SyncWBSFromSQLServer()
‘ 接続文字列(環境に合わせて変更してください)
Const CONN_STR As String = “Provider=SQLOLEDB;Server=YOUR_SERVER;Database=YOUR_DB;Uid=YOUR_USER;Pwd=YOUR_PASSWORD;”

Dim conn As Object
Dim rs As Object
Dim sql As String

‘ パフォーマンス最大化のため、画面描画と自動計算を一時停止
Application.ScreenUpdating = False
Application.Calculation = pjManual

On Error GoTo ErrorHandler

‘ 1. 既存タスクのクリア(必要に応じて差分更新に変更可能)
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”)
conn.Open CONN_STR

sql = “SELECT TaskUID, WBSLevel, TaskName, DurationDays, PredecessorUIDs ” & _
“FROM dbo.ProjectTasks WHERE ProjectCode = ‘PRJ-001’ ORDER BY TaskUID ASC;”

Set rs = CreateObject(“ADODB.Recordset”)
rs.Open sql, conn, 1, 1 ‘ adOpenKeyset, adLockReadOnly

If rs.EOF Then
MsgBox “同期対象のタスクが見つかりませんでした。”, vbExclamation, “同期エラー”
GoTo Cleanup
End If

‘ DBのTaskUIDと、Project上のTaskオブジェクト(またはUniqueID)を紐付けるためのDictionary
Dim dictIdMap As Object
Set dictIdMap = CreateObject(“Scripting.Dictionary”)

Dim currentTask As Task
Dim dbUid As Long
Dim wbsLevel As Long
Dim prevLevel As Long
prevLevel = 0

‘ ———————————————————————
‘ フェーズ1: タスクの生成とインデント(階層構造)の構築
‘ ———————————————————————
Do While Not rs.EOF
dbUid = rs(“TaskUID”).Value
wbsLevel = rs(“WBSLevel”).Value

‘ タスクの追加
Set currentTask = ActiveProject.Tasks.Add(rs(“TaskName”).Value)
currentTask.Duration = rs(“DurationDays”).Value & “d”

‘ 階層(アウトライン)の調整
‘ ※ProjectのOutlineIndent / OutlineOutdentは相対的なので、レベル差分で制御する
If wbsLevel > prevLevel Then
Dim i As Long
For i = 1 To (wbsLevel – prevLevel)
currentTask.OutlineIndent
Next i
ElseIf wbsLevel < prevLevel Then Dim j As Long For j = 1 To (prevLevel - wbsLevel) currentTask.OutlineOutdent Next j End If ' マッピング用Dictionaryに登録(DBのIDをキーに、ProjectのUniqueIDをバインド) dictIdMap.Add dbUid, currentTask.UniqueID prevLevel = wbsLevel rs.MoveNext Loop ' --------------------------------------------------------------------- ' フェーズ2: 依存関係(前提条件)の構築 ' ※全タスクが生成された後に行うことで、参照エラーを完全に防ぐ ' --------------------------------------------------------------------- rs.MoveFirst Do While Not rs.EOF dbUid = rs("TaskUID").Value Dim predString As String predString = Nz(rs("PredecessorUIDs").Value, "") If Trim(predString) <> “” Then
Dim targetTaskUniqueID As Long
targetTaskUniqueID = dictIdMap(dbUid)

Dim targetTask As Task
Set targetTask = ActiveProject.Tasks.UniqueID(targetTaskUniqueID)

‘ カンマ区切りの先行タスクIDを分解してリンクを張る
Dim predIds() As String
predIds = Split(predString, “,”)

Dim p As Variant
For Each p In predIds
If dictIdMap.Exists(CLng(Trim(p))) Then
Dim predTaskUniqueID As Long
predTaskUniqueID = dictIdMap(CLng(Trim(p)))

Dim predTask As Task
Set predTask = ActiveProject.Tasks.UniqueID(predTaskUniqueID)

‘ リンクの追加 (標準はFS: Finish-to-Start)
targetTask.TaskDependencies.Add ToTask:=targetTask, FromTask:=predTask, Type:=pjLinkFinishToStart
End If
Next p
End If

rs.MoveNext
Loop

Cleanup:
‘ リソース解放と設定の復元
If Not rs Is Nothing Then
If rs.State Then rs.Close
End If
If Not conn Is Nothing Then
If conn.State Then conn.Close
End If

Set rs = Nothing
Set conn = Nothing
Set dictIdMap = Nothing

Application.Calculation = pjAutomatic
Application.ScreenUpdating = True

MsgBox “SQL ServerからのWBS同期が正常に完了しました。”, vbInformation, “完了”
Exit Sub

ErrorHandler:
Application.Calculation = pjAutomatic
Application.ScreenUpdating = True
MsgBox “予期せぬエラーが発生しました: ” & Err.Description, vbCritical, “致命的エラー”
End Sub

‘ 簡易Null値ハンドラ
Private Function Nz(ByVal val As Variant, ByVal replacement As Variant) As Variant
If IsNull(val) Then
Nz = replacement
Else
Nz = val
End If
End Function

4. チーフアーキテクトが教える「現場で活きる」実装の急所

上記のコードには、エンタープライズ開発で生き残るための「非機能要件」の知見が凝縮されている。

① 画面描画と自動計算の完全遮断(`ScreenUpdating` & `Calculation`)

MS Projectは、UIと計算エンジンが密結合している。タスクを1万件操作する際、これらを有効にしたままだと数分〜数十分のロスが生じる。
`Application.Calculation = pjManual` に設定することで、全ての構造構築が終わるまでバックグラウンド計算を完全にシャットアウトし、処理速度を劇的に向上させている(体感で10倍以上の差が出る)。

② 2パス(2段階)処理による依存関係の完全解決

前述の通り、DBから取得した順番通りにその場でリンクを張ろうとすると、「まだ生成されていない後続・先行タスク」を参照してしまい破綻する。

  • パス1: 全タスクをフラットに、かつインデントを整えながら作成し、DBのIDとProjectの `UniqueID` を `Scripting.Dictionary` に記憶する。
  • パス2: 全タスクの生成完了後に、Dictionaryを引いて安全に `TaskDependencies.Add` を実行する。

この設計思想により、DB側の並び順に依存しない堅牢なリンク構築が可能になる。

③ 相対インデント制御の罠へのアプローチ

MS Projectの `OutlineIndent` メソッドは、「現在の行を一つ下位の階層にする」という相対的な動作をする。
そのため、単に「WBSレベルが2だからレベル2にする」というコードを書くと大事故が起きる。
必ず「前回のレベル(`prevLevel`)」との差分を計算し、プラスなら `OutlineIndent` を差分回数だけループ実行、マイナスなら `OutlineOutdent` を走らせるという状態管理ロジックが必須となる。

5. おわりに

VBAを「おもちゃのマクロ言語」と侮るなかれ。オブジェクトモデルの本質を理解し、メモリ管理とトランザクション的思考(パスの分離)を取り入れれば、SQL Serverのような堅牢なRDBMSとMS Projectをシームレスに繋ぐ強力なエンタープライズ・インテグレーション基盤へと昇華できる。

手作業による転記という不毛な業務から現場を解放し、真に価値のあるプロジェクトマネジメントにリソースを集中させること。それこそが、我々エンジニアが果たすべき使命である。

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