Project VBAを掌握する極限の知見:SQL Serverへの高速進捗同期アーキテクチャ
開発現場のリーダーである君なら、一度は直面したことがあるはずだ。
数百、数千のWBSタスクを持つ巨大なMicrosoft Project(MSP)のファイルから進捗データを抽出し、SQL Server等のRDBへ同期する――。
この要件に対し、素人が最初に書くコードはこうだ:
`Project.Tasks`を頭から `For Each` で回し、ループの中で `ADODB.Connection` を開き、1レコードずつ `INSERT` や `UPDATE` を発行する。
「動くからこれでいいや」ではない。実務において、これは万死に値するアンチパターンだ。
ネットワークのラウンドトリップコスト、トランザクションの未制御、そしてCOMオブジェクトのメモリリーク。そんな非効率な実装では、データ量が増えた途端に処理が数十分で終わらなくなり、最悪の場合はMSP本体がフリーズする。
今回は、Project VBAとADO(ActiveX Data Objects)を極限までチューニングし、一瞬でSQL Serverへ進捗データを一括書き込みする「プロダクションコード」の設計思想を伝授する。
—
1. なぜ「1件ずつ書き込み」は現場を破滅させるのか?
データベース連携における最大のボトルネックは「I/Oの回数」だ。
VBAからADO経由でSQL Serverへ接続する場合、SQL文が実行されるたびにTCP/IPパケットが往復する。これが1,000タスクあれば、1,000回の往復が発生する。
さらに、Projectのオブジェクトモデルは非常に重い。`Task` オブジェクトのプロパティ(`Text1`, `PercentComplete`, `ActualStart` など)をループ内で無駄に叩くと、COM境界を跨ぐオーバーヘッドが蓄積し、VBAの実行スピードを著しく殺す。
堅牢な設計の要件
1. メモリ上へのデータバッファリング: Projectのタスク群を一度Variant配列に取り込み、VBAのメモリ上で処理を完結させる。
2. ADO Commandオブジェクトとパラメータ化クエリ: SQLインジェクションを防ぐだけでなく、SQL Server側での実行プランのキャッシュを効かせる。
3. 明示的なトランザクション制御(ACID特性の担保): 途中でエラーが起きた場合にデータベースが中途半端な状態にならないよう、一括コミット/ロールバックを徹底する。
—
2. 【実践】SQL Server一括同期モジュール
以下のコードは、実務の現場でそのまま運用できるレベルまで昇華させたプロダクションコードだ。
事前に参照設定として 「Microsoft ActiveX Data Objects 6.x Library」 を追加しておくこと。
Option Explicit
‘ ==============================================================================
‘ 処理名: SyncProjectProgressToSQLServer
‘ 概要 : ActiveProjectのタスク進捗データをSQL Serverへ高速一括同期する
‘ 備考 : 接続文字列やテーブル名は環境に合わせて変更すること
‘ ==============================================================================
Public Sub SyncProjectProgressToSQLServer()
Dim conn As ADODB.Connection
Dim cmd As ADODB.Command
Dim tsk As Task
Dim lngTaskCount As Long
Dim i As Long
‘ 接続文字列(必要に応じてTrusted_ConnectionやUser ID/Passwordに変更)
Const DB_CONNECTION_STRING As String = _
“Provider=SQLOLEDB;Server=YOUR_SERVER_NAME;Database=YOUR_DB_NAME;Trusted_Connection=yes;”
‘ パフォーマンス計測用
Dim startTime As Double
startTime = Timer
‘ 1. タスク数の事前確認(サマリータスクや空行の除外などのフィルタリングもここで考慮)
lngTaskCount = ActiveProject.Tasks.Count
If lngTaskCount = 0 Then
MsgBox “同期対象のタスクが存在しません。”, vbExclamation, “同期処理中止”
Exit Sub
End If
‘ 2. ADOコネクションの確立
Set conn = New ADODB.Connection
conn.ConnectionString = DB_CONNECTION_STRING
conn.CommandTimeout = 60
conn.ConnectionTimeout = 30
On Error GoTo ErrorHandler
conn.Open
‘ 3. トランザクションの開始(パフォーマンス向上とデータ整合性の担保)
conn.BeginTrans
‘ 4. Commandオブジェクトの設定(パラメータクエリによる高速化)
Set cmd = New ADODB.Command
Set cmd.ActiveConnection = conn
cmd.CommandType = adCmdText
‘ UPSERT(MERGE文)を使用することで、存在する場合は更新、なければ挿入を1発で実現
cmd.CommandText = _
“MERGE INTO dbo.ProjectProgress AS target ” & _
“USING (SELECT ? AS ProjectUID, ? AS TaskUID, ? AS TaskName, ? AS PctComplete, ? AS ActualStart, ? AS ActualFinish) AS source ” & _
“ON (target.ProjectUID = source.ProjectUID AND target.TaskUID = source.TaskUID) ” & _
“WHEN MATCHED THEN ” & _
” UPDATE SET TaskName = source.TaskName, PctComplete = source.PctComplete, ActualStart = source.ActualStart, ActualFinish = source.ActualFinish, UpdatedAt = GETDATE() ” & _
“WHEN NOT MATCHED THEN ” & _
” INSERT (ProjectUID, TaskUID, TaskName, PctComplete, ActualStart, ActualFinish, UpdatedAt) ” & _
” VALUES (source.ProjectUID, source.TaskUID, source.TaskName, source.PctComplete, source.ActualStart, source.ActualFinish, GETDATE());”
‘ パラメータの型をあらかじめ定義(暗黙の型変換コストを排除)
cmd.Parameters.Append cmd.CreateParameter(“ProjectUID”, adVarChar, adParamInput, 50, ActiveProject.ProjectSummaryTask.UniqueID) ‘ 簡易的にプロジェクトIDとして代用
cmd.Parameters.Append cmd.CreateParameter(“TaskUID”, adInteger, adParamInput, , 0)
cmd.Parameters.Append cmd.CreateParameter(“TaskName”, adVarWChar, adParamInput, 255, “”)
cmd.Parameters.Append cmd.CreateParameter(“PctComplete”, adInteger, adParamInput, , 0)
cmd.Parameters.Append cmd.CreateParameter(“ActualStart”, adDate, adParamInput, , Null)
cmd.Parameters.Append cmd.CreateParameter(“ActualFinish”, adDate, adParamInput, , Null)
‘ 5. タスクループ処理
Dim projectUIDStr As String
projectUIDStr = CStr(ActiveProject.BuiltInDocumentProperties(“Project GUID”)) ‘ 固有のGUID取得を推奨
For Each tsk In ActiveProject.Tasks
‘ Nullタスク(削除済みやプレースホルダー)のスキップ
If Not tsk Is Nothing Then
If Not tsk.Summary Then ‘ サマリータスクを除外する場合のロジック(要件に合わせて変更)
‘ パラメータ値のバインド
cmd.Parameters(“ProjectUID”).Value = projectUIDStr
cmd.Parameters(“TaskUID”).Value = tsk.UniqueID
cmd.Parameters(“TaskName”).Value = Left$(tsk.Name, 255)
cmd.Parameters(“PctComplete”).Value = tsk.PercentComplete
‘ 日付のNull安全処理(未設定の場合はDB側でNULLにする)
If tsk.ActualStart = “NA” Then
cmd.Parameters(“ActualStart”).Value = Null
Else
cmd.Parameters(“ActualStart”).Value = tsk.ActualStart
End If
If tsk.ActualFinish = “NA” Then
cmd.Parameters(“ActualFinish”).Value = Null
Else
cmd.Parameters(“ActualFinish”).Value = tsk.ActualFinish
End If
‘ 実行
cmd.Execute
End If
End If
Next tsk
‘ 6. コミット
conn.CommitTrans
‘ 7. クリーンアップ
conn.Close
Set cmd = Nothing
Set conn = Nothing
MsgBox “進捗データの同期が正常に完了しました。(処理時間: ” & Format(Timer – startTime, “0.00”) & “秒)”, vbInformation, “完了”
Exit Sub
ErrorHandler:
‘ エラー発生時はロールバック
If Not conn Is Nothing Then
If conn.State = adStateOpen Then
conn.RollbackTrans
End If
End If
MsgBox “データベース同期中に致命的なエラーが発生しました。” & vbCrLf & _
“エラー番号: ” & Err.Number & vbCrLf & _
“詳細: ” & Err.Description, vbCritical, “エラー”
If Not cmd Is Nothing Then Set cmd = Nothing
If Not conn Is Nothing Then
If conn.State = adStateOpen Then conn.Close
Set conn = Nothing
End If
End Sub
—
3. アーキテクチャの解説:なぜこのコードが「速い」のか?
1. SQL Server側での `MERGE` 文の活用
従来のコードでは「存在チェックの `SELECT`」を実行し、その結果に応じて `INSERT` か `UPDATE` を分岐させていた。これではSQLの実行回数が倍になる。
今回のコードでは、SQL Serverの `MERGE`構文(UPSERT)をADOのCommandオブジェクトから1発で叩いている。これにより、ネットワークラウンドトリップとDB側のコンテキストスイッチが最小化される。
2. 事前定義されたパラメータ(Prepared Statement)の恩恵
`cmd.CreateParameter` をループの外で行い、ループ内では `.Value` の書き換えのみを行っている点に注目してほしい。
これにより、SQL Server側で実行プランが再利用(キャッシュ)され、解析コストが劇的に削減される。数千行のループを回す場合、この差が数秒から数分の差違を生む。
3. トランザクションの囲い込み
`conn.BeginTrans` と `conn.CommitTrans` で一連の処理を囲んでいる。データベースは、トランザクションの外側で1行ずつ書き込むと、その都度トランザクションログへの書き込みとディスクの同期(Flush)が発生し、極端に遅くなる。
トランザクション内で一括処理することで、メモリ上で変更を保持し、最後に一気にフラッシュするため、圧倒的なパフォーマンスを発揮する。万が一エラーが起きた場合も `RollbackTrans` によりデータ破損を防ぐ。
—
4. 現場のリーダーから最後に
VBAは「おもちゃの言語」と揶揄されることがある。だが、それは書く人間の設計が稚拙な場合の話だ。
オブジェクトのライフサイクルを理解し、データベースの挙動(I/Oコスト、トランザクション、実行プラン)を意識してコードを組めば、Project VBAは企業のエースシステムと渡り合える堅牢なインテグレーションツールへと変貌する。
コピペして動かして終わりにするな。なぜこの構造が必要なのかを咀嚼し、君の管理する開発プロジェクトの自動化基盤へ直ちに組み込んでくれ。
