【入門編】プロ向け:大規模プロジェクトにおける制約条件の変更履歴をSQL Serverへ保存する – Project VBA解析バイブル

スポンサーリンク
スポンサーリンク

Project VBAを掌握する極限の知見:大規模プロジェクトのガバナンスを守る「制約条件変更ログ」の自動記録術

こんにちは。現場の荒波を乗り越え、自動化の力でビジネスを加速させるエンジニアの皆さん。
今日は、マクロの記録から一歩踏み出し、「システムとしての堅牢性」を意識し始めたあなたに贈る、少しだけ高度で、かつ実務において極めて重要なテーマをお話しします。

テーマは「プロジェクトの制約条件(Constraint)の変更履歴をSQL Serverへ外部保存する」です。

「えっ、ただのスケジュール管理なのに、なぜ履歴をDBに保存する必要があるの?」と思うかもしれません。しかし、数百人、数千人が関わる大規模プロジェクトでは、「なぜかスケジュールが動いた。誰かが制約条件(Must Start Onなど)を無理に設定したせいだ」という事象が頻発します。この「犯人探し」ではなく「原因究明」と「ガバナンス(統制)」を行うための仕組みこそが、プロのエンジニアが作るべき世界です。

ここをクリアすれば、あなたは「ツールを作る人」から「仕組みを作るアーキテクト」へと進化できるはずですよ。

1. なぜ「制約条件」のログが必要なのか?

MS Projectのスケジュール計算エンジンは非常に強力です。しかし、その計算の根拠となる「制約条件」は、ユーザーの手によって容易に変更できてしまいます。

  • ASAP (As Soon As Possible): できるだけ早く(標準的)
  • ALAP (As Late As Possible): できるだけ遅く
  • Must Start On: 指定した日に必ず開始(強力な制約)

例えば、あるタスクの制約を「ASAP」から「Must Start On」に変えた瞬間、そのタスクはスケジュールの柔軟性を失い、後続のタスクを押し下げます。これが積み重なると、プロジェクト全体の整合性が崩壊します。

「いつ、誰が、どのタスクに、どのような制約を課したのか」
これがブラックボックス化しているプロジェクトは、管理不能なプロジェクトです。これをSQL Serverという「外部の証跡」に刻む仕組みを作っていきましょう。

2. アーキテクチャの設計図

今回構築するのは、以下のようなデータフローです。

1. Trigger: MS Project内でタスクの制約条件が変更される。
2. Detection: VBAが「前回の状態」と「現在の状態」を比較し、変化を検知。
3. Data Collection: 変更内容(タスク名、ID、ユーザー名、日時、変更前後の制約)を収集。
4. Transmission: ADODB(ActiveX Data Objects)を使用してSQL Serverへ送信。
5. Storage: SQL Serverのログテーブルに永続化。

準備するもの:SQL Server側のテーブル定義

まずは、受け皿となるテーブルをSQL Server側に用意しておきましょう。

-- 制約条件変更ログを格納するテーブル
CREATE TABLE ProjectConstraintLogs (
    LogID INT IDENTITY(1,1) PRIMARY KEY,
    ProjectName NVARCHAR(255),
    TaskID INT,
    TaskName NVARCHAR(255),
    OldConstraint NVARCHAR(50),
    NewConstraint NVARCHAR(50),
    ChangedBy NVARCHAR(100),
    ChangedAt DATETIME DEFAULT GETDATE()
);

3. 実装:極限のVBAコード

ここでは、プロフェッショナルな実装として「Dictionaryオブジェクトを用いた状態比較」という手法を使います。
MS Projectには「制約が変わった瞬間」を直接捉えるイベントが乏しいため、`Project_Calculate`(計算実行時)に、メモリ上の「前回状態」と「現在の状態」を照合するアプローチをとります。

※このコードを動かすには、VBAの参照設定で 「Microsoft Scripting Runtime」 と 「Microsoft ActiveX Data Objects x.x Library」 にチェックを入れてください。

Option Explicit

' --- グローバル変数:前回の制約状態を保持するための辞書 ---
' プロジェクトのライフサイクル中、メモリ上に「タスクID: 制約タイプ」を保持します
Private PreviousConstraints As Scripting.Dictionary

' --- 設定定数 ---
Private Const DB_CONN_STR As String = "Provider=SQLOLEDB;Data Source=YOUR_SERVER;Initial Catalog=YOUR_DB;User ID=YOUR_USER;Password=YOUR_PW;"

''' <summary>
''' プロジェクトが開いたときに初期化
''' </summary>
Private Sub Project_Open()
    Set PreviousConstraints = New Scripting.Dictionary
    Call InitializeState
    Debug.Print "System: Constraint Monitoring Initialized."
End Sub

''' <summary>
''' プロジェクトの再計算時に実行される
''' </summary>
Private Sub Project_Calculate()
    On Error GoTo ErrorHandler
    
    Dim t As Task
    Dim currentConstraint As String
    Dim oldConstraint As String
    
    ' 1. 現在のタスクの状態をスキャン
    For Each t In ActiveProject.Tasks
        If Not t Is Nothing Then
            ' 制約タイプを文字列として取得(例: "pjTaskConstraintTypeAsSoonAsPossible")
            currentConstraint = GetConstraintName(t.ConstraintType)
            
            ' 2. 変更検知ロジック
            ' 辞書にタスクIDが存在するか確認
            If PreviousConstraints.Exists(t.ID) Then
                oldConstraint = PreviousConstraints(t.ID)
                
                ' 前回の値と現在の値が異なる場合、変更とみなす
                If oldConstraint <> currentConstraint Then
                    ' ログ出力実行
                    Call LogConstraintChange(t, oldConstraint, currentConstraint)
                    
                    ' 辞書を更新
                    PreviousConstraints(t.ID) = currentConstraint
                End If
            Else
                ' 初回スキャン時は辞書に登録するだけ
                PreviousConstraints.Add t.ID, currentConstraint
            End If
        End If
    Next t

    Exit Sub

ErrorHandler:
    Debug.Print "Error in Project_Calculate: " & Err.Description
End Sub

''' <summary>
''' 制約タイプ(列挙型)を人間が読める文字列に変換
''' </summary>
Private Function GetConstraintName(ByVal cType As Long) As String
    Select Case cType
        Case 0: GetConstraintName = "ASAP"
        Case 1: GetConstraintName = "ASAP" ' MS Projectの内部定義に準拠
        Case 2: GetConstraintName = "ALAP"
        Case 3: GetConstraintName = "StartNoFinish"
        Case 4: GetConstraintName = "StartOn"
        Case 5: GetConstraintName = "StartNoStart"
        Case 6: GetConstraintName = "StartMustStartOn"
        ' 必要に応じて追加
        Case Else: GetConstraintName = "Other(" & cType & ")"
    End Select
End Function

''' <summary>
''' SQL Serverへログを書き込む
''' </summary>
Private Sub LogConstraintChange(ByRef targetTask As Task, ByVal oldC As String, ByVal newC As String)
    Dim conn As ADODB.Connection
    Dim cmd As ADODB.Command
    Dim userName As String
    
    userName = Environ("USERNAME") ' Windowsログインユーザー名を取得
    
    Set conn = New ADODB.Connection
    Set cmd = New ADODB.Command
    
    On Error GoTo DBError
    
    ' DB接続
    conn.Open DB_CONN_STR
    
    ' SQL実行(SQLインジェクション対策としてパラメータクエリを使用)
    With cmd
        .ActiveConnection = conn
        .CommandText = "INSERT INTO ProjectConstraintLogs (ProjectName, TaskID, TaskName, OldConstraint, NewConstraint, ChangedBy) " & _
                       "VALUES (?, ?, ?, ?, ?, ?)"
        .CommandType = adCmdText
        
        ' パラメータのバインド
        .Parameters.Append .CreateParameter("@p1", adVarWChar, adParamInput, 255, ActiveProject.Name)
        .Parameters.Append .CreateParameter("@p2", adInteger, adParamInput, , targetTask.ID)
        .Parameters.Append .CreateParameter("@p3", adVarWChar, adParamInput, 255, targetTask.Name)
        .Parameters.Append .CreateParameter("@p4", adVarWChar, adParamInput, 50, oldC)
        .Parameters.Append .CreateParameter("@p5", adVarWChar, adParamInput, 50, newC)
        .Parameters.Append .CreateParameter("@p6", adVarWChar, adParamInput, 100, userName)
        
        .Execute
    End With
    
    Debug.Print "Log Success: Task " & targetTask.ID & " changed from " & oldC & " to " & newC
    
CleanUp:
    If Not conn Is Nothing Then
        If conn.State = adStateOpen Then conn.Close
    End If
    Set conn = Nothing
    Set cmd = Nothing
    Exit Sub

DBError:
    Debug.Print "DB Error: " & Err.Description
    Resume CleanUp
End Sub

''' <summary>
''' 初期状態を辞書に格納
''' </summary>
Private Sub InitializeState()
    Dim t As Task
    Set PreviousConstraints = New Scripting.Dictionary
    For Each t In ActiveProject.Tasks
        If Not t Is Nothing Then
            PreviousConstraints.Add t.ID, GetConstraintName(t.ConstraintType)
        End If
    Next t
End Sub

4. プロが教える「陥りやすい罠」とパフォーマンスの最適化

このコードをそのまま大規模プロジェクト(タスク数が数万件)に適用すると、一つの問題に直面します。それは「計算負荷」です。

⚠️ 罠1:Project_Calculate の過剰な実行

`Project_Calculate` は、ユーザーが日付を一つ動かすたびに、あるいは計算エンジンがバックグラウンドで動くたびに呼び出されます。ここで全タスクをループ回すと、MS Projectが「重い!」と悲鳴を上げます。

【解決策】

  • 差分更新の検討: 今回は `Dictionary` を使っていますが、これは非常に高速です。しかし、それでも重い場合は、`Project_Calculate` ではなく、特定の「保存ボタン」や「チェックボタン」をユーザーに押させる運用にするのが、実務上は最も安全です。
  • タスクの絞り込み: 全タスクではなく、特定の階層や、マイルストーンのみを監視対象にするなどのフィルタリングを検討してください。

⚠️ 罠2:DB接続のオーバーヘッド

ループの中で毎回 `conn.Open` と `conn.Close` を繰り返すのは、パフォーマンスの天敵です。

【解決策】

  • 大量に変更がある可能性がある場合は、一度メモリ上の `Collection` や `Array` に変更ログを溜め込み、ループ終了後に一括で(Bulk Insert的に)DBへ送る設計にアップグレードしましょう。

⚠️ 罠3:SQLインジェクション

`”INSERT INTO … VALUES (‘” & taskName & “‘)”` のような文字列結合によるSQL構築は、絶対に避けてください。タスク名に `’`(シングルクォート)が入っただけでエラーになり、悪意のある操作には脆弱性となります。
上記のコードでは、プロの鉄則である 「パラメータクエリ(`Parameters.Append`)」 を使用しています。

まとめ:ここをクリアすれば、あなたはもう「初学者」ではない

いかがでしたか?
単に「マクロで値を書き換える」だけでなく、「状態を保持し、変化を検知し、外部の信頼できる場所に記録する」。この一連の流れは、業務システム開発における基本にして極意です。

1. オブジェクトのライフサイクル(Dictionaryの保持)を理解すること
2. イベントの特性(Calculateの頻度)を理解すること
3. データの整合性とセキュリティ(パラメータクエリ)を意識すること

これらを意識できるようになったあなたは、もう立派なエンジニアの入り口に立っています。
一歩ずつ、コードに魂を込めていきましょう。応援していますよ!

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