スケジュール計算エンジンの「沈黙」を破る:大規模プロジェクトにおける制約条件変更の監査ログ実装
大規模プロジェクトの進捗管理において、Microsoft Projectの「制約条件(Constraints)」は、スケジュールの整合性を保つための生命線であると同時に、工程の「隠蔽」を可能にする危ういレバーでもある。
ASAP(可能な限り早く)から「Must Start On(開始日固定)」への変更、あるいはALAP(可能な限り遅く)への操作。これらはクリティカルパスを人為的に操作し、遅延をスケジュール計算エンジンの裏側に隠蔽する手段となり得る。ガバナンスが求められるエンタープライズ環境において、これらの「計算ロジックへの介入」を、誰が、いつ、どのタスクに対して行ったのかを、MS Projectの外側(SQL Server)へ、かつパフォーマンスを損なわずに記録する仕組みは、シニアエンジニアが構築すべき必須の防壁である。
本稿では、単なるイベントハンドラの書き方ではない。オブジェクトのライフサイクル、メモリ管理、そして大規模データに対するDB I/Oの最適化を考慮した、極限の監査ロギング・アーキテクチャを詳解する。
1. アーキテクチャの設計思想:Event Stormへの対抗策
MS Projectの`Project_Change`イベントは、ユーザーがセルを一つ書き換えるたびに、あるいは計算エンジンが再計算を行うたびに、凄まじい頻度で発火する。このイベント内で直接SQL Serverへ接続し、`INSERT`文を発行するような設計は、初歩的なミスである。それはアプリケーションのフリーズ(UIスレッドのブロッキング)を招き、ユーザー体験を破壊する。
我々が実装すべきは、「状態比較型(State-Comparison)監査モデル」である。
1. State Tracking: 各タスクの「現在の制約条件」をメモリ上の辞書(Scripting.Dictionary)に保持する。
2. Delta Detection: `Change`イベント発生時、変更されたオブジェクトの制約条件が、メモリ上の保持値と「実際に」異なる場合のみ、変更とみなす。
3. Asynchronous-like I/O: SQL Serverへの書き込みは、接続のオーバーヘッドを最小化するため、ADODBの接続プールを意識したシングルトン・パターンを用いる。
2. 実装:高信頼性監査エンジンの構築
以下に、堅牢な実装例を示す。ここでは、メモリリークを防ぐための明示的なオブジェクト解放と、64bit環境への対応(`PtrSafe`)を前提とした設計を行う。
A. データベース・アクセス・レイヤー (modDatabase)
SQL Serverへの接続は、メソッドを呼ぶたびに`Open/Close`を繰り返してはならない。接続オブジェクトを保持し、トランザクションの整合性を確保する。
' --- modDatabase ---
Option Explicit
Private m_conn As ADODB.Connection
Private Const CONN_STR As String = "Provider=SQLOLEDB;Data Source=YOUR_SERVER;Initial Catalog=ProjectAuditDB;Integrated Security=SSPI;"
''' <summary>
''' SQL Serverへの接続をシングルトン的に管理する
''' </summary>
Public Function GetConnection() As ADODB.Connection
If m_conn Is Nothing Then
Set m_conn = New ADODB.Connection
m_conn.ConnectionString = CONN_STR
m_conn.Open
End If
Set GetConnection = m_conn
End Function
''' <summary>
''' 監査ログを記録する。パラメータクエリを用い、SQLインジェクションを完全に防御する。
''' </summary>
Public Sub LogConstraintChange(ByVal taskID As Long, ByVal taskName As String, _
ByVal oldConst As Long, ByVal newConst As Long, _
ByVal userName As String)
On Error GoTo ErrHandler
Dim cmd As ADODB.Command
Set cmd = New ADODB.Command
With cmd
.ActiveConnection = GetConnection()
.CommandText = "INSERT INTO TaskAuditLog (TaskID, TaskName, OldConstraint, NewConstraint, ChangedBy, ChangeDate) " & _
"VALUES (?, ?, ?, ?, ?, GETDATE())"
.CommandType = adCmdText
' パラメータのバインド(型を厳密に指定)
.Parameters.Append .CreateParameter("@id", adBigInt, adParamInput, , taskID)
.Parameters.Append .CreateParameter("@name", adVarChar, adParamInput, 255, taskName)
.Parameters.Append .CreateParameter("@old", adInteger, adParamInput, , oldConst)
.Parameters.Append .CreateParameter("@new", adInteger, adParamInput, , newConst)
.Parameters.Append .CreateParameter("@user", adVarChar, adParamInput, 100, userName)
.Execute
End With
CleanUp:
Set cmd = Nothing
Exit Sub
ErrHandler:
' 本来はここでエラーログをローカルファイル等に書き出すべき
Debug.Print "DB Error: " & Err.Description
Resume CleanUp
End Sub
''' <summary>
''' アプリケーション終了時に明示的に接続を閉じる
''' </summary>
Public Sub CloseConnection()
If Not m_conn Is Nothing Then
If m_conn.State = adStateOpen Then m_conn.Close
Set m_conn = Nothing
End If
End Sub
B. 状態管理レイヤー (clsTaskState)
タスクごとの制約条件をメモリにキャッシュするためのクラスである。
' --- clsTaskState (Class Module) ---
Option Explicit
' タスクの制約条件を保持
Public ConstraintType As Long
' タスクの一意識別子(MS ProjectのIDは再利用されるため、本来はUIDを使うべき)
Public TaskUID As Long
C. イベント・インターセプター (ThisProject)
ここが心臓部である。`Project_Change`イベントにおいて、変更が「制約条件」に関するものか、かつ「値が実際に変わったか」を判定する。
' --- ThisProject ---
Option Explicit
' タスクの状態を保持する辞書(UIDをキーとする)
Private m_stateCache As Scripting.Dictionary
' 状態管理用クラスのインスタンス
Private m_states As Collection
Private Sub Project_Open(ByVal pj As MSProject.Project)
Set m_stateCache = New Scripting.Dictionary
Set m_states = New Collection
' 初期状態のスナップショットを作成
Dim t As Task
For Each t In pj.Tasks
If Not t Is Nothing Then
CaptureState t
End If
Next t
End Sub
Private Sub Project_Change(ByVal pj As MSProject.Project)
Dim t As Task
' 注意: Changeイベント自体が大量に発生するため、
' 変更されたオブジェクトを特定するロジックが必要。
' MS ProjectのAPI制限により、変更されたタスクを直接特定するのは難しいため、
' 変更が疑われるオブジェクトのプロパティを検証する。
' ※実務上の高度な実装では、Selectionオブジェクトや
' 変更直前の値を保持するロジックを組み合わせる。
On Error Resume Next
' ここでは簡略化のため、現在選択されているタスク、
' もしくは変更が発生したコンテキストを想定
Set t = ActiveProject.Tasks(Selection.Tasks(1).ID)
If t Is Nothing Then Exit Sub
On Error GoTo 0
Dim currentConst As Long
currentConst = t.ConstraintType
Dim oldConst As Long
oldConst = GetCachedConstraint(t.UniqueID)
' 変化の検知
If currentConst <> oldConst Then
' DBへログ出力
Call modDatabase.LogConstraintChange(t.ID, t.Name, oldConst, currentConst, Environ("USERNAME"))
' キャッシュを更新
UpdateCache t
End If
End Sub
' --- 内部補助関数 ---
Private Sub CaptureState(ByVal t As Task)
Dim state As clsTaskState
Set state = New clsTaskState
state.TaskUID = t.UniqueID
state.ConstraintType = t.ConstraintType
m_stateCache.Add t.UniqueID, state
End Sub
Private Function GetCachedConstraint(ByVal uid As Long) As Long
If m_stateCache.Exists(uid) Then
GetCachedConstraint = m_stateCache(uid).ConstraintType
Else
' キャッシュにない場合は現在の値を返し、次回のために登録
GetCachedConstraint = -1 ' 未登録フラグ
End If
End Function
Private Sub UpdateCache(ByVal t As Task)
If Not m_stateCache.Exists(t.UniqueID) Then
CaptureState t
Else
m_stateCache(t.UniqueID).ConstraintType = t.ConstraintType
End If
End Sub
Private Sub Project_BeforeClose(Cancel As Boolean)
' メモリ解放の極致
Set m_stateCache = Nothing
Set m_states = Nothing
Call modDatabase.CloseConnection
End Sub
3. プロフェッショナルのための最適化知見
オブジェクトのライフサイクル管理
VBAにおいて、`Set obj = Nothing` は単なる作法ではない。特に `ADODB.Command` や `Scripting.Dictionary` のような外部COMコンポーネントを扱う際、スコープを抜けても参照カウントが残っていると、MS Projectプロセスが終了しても `MSProject.exe` がタスクマネージャーに残り続ける(ゾンビプロセス化)。本実装では、`CleanUp`ラベルを用いた明示的な解放を徹底している。
パフォーマンスへの影響(Complexity Analysis)
`Project_Change` 内でのループ処理は $O(n)$ のコストを強いる。タスク数が10,000を超える大規模プロジェクトでは、全タスクをループで回す実装は致命的である。本稿の `m_stateCache` による `Dictionary`(ハッシュテーブル)を用いたアクセスは $O(1)$ であり、計算量の爆発を回避している。
セキュリティとガバナンス
SQL Serverへの接続に `Integrated Security=SSPI` を使用している点は重要である。これにより、アプリケーション側にDBパスワードをハードコードする必要がなくなり、Windows認証による厳格なアクセス制御が可能となる。これは、社内システム管理者にとって、監査ログ自体の改ざんを防ぐための最低条件である。
結論
プロジェクトのスケジュールは、単なるデータの集合体ではない。それは意思決定の記録である。
制約条件という「エンジンの設定」を監視することは、プロジェクトの透明性を担保するための最も技術的で、かつ政治的なアプローチの一つである。この実装を通じて、君たちのシステムは単なるツールから、信頼に足る「ガバナンス・プラットフォーム」へと昇華するだろう。
