【深層】Project VBA × SQL Server:分散するMPP情報を統合管理する「不滅のログ・アーキテクチャ」
Microsoft Project(以下、Project)は、単なるスケジュール管理ツールではない。それは企業の「時間」と「リソース」が結晶化した巨大なバイナリデータだ。しかし、.mppというファイル形式に閉じ込められた情報は、外部から観測できない「ブラックボックス」に陥りやすい。
シニアエンジニアやシステム管理者に課せられた使命は、このブラックボックスをこじ開け、組織全体のガバナンス下に置くことだ。今回は、Projectの保存アクションをトリガーとし、プロジェクトのメタデータやベースライン情報をSQL Serverへ自動的に同期する、堅牢なログシステムの構築手法を詳説する。
—
1. 永続化への設計思想:なぜSQL Serverなのか
Excelやテキストファイルへのログ出力は、小規模な実験には適している。しかし、エンタープライズ環境では「同時実行制御」「トランザクションの整合性」「長期的なクエリ性能」が不可欠だ。
Project VBAからSQL Serverへデータを流し込む際、我々が考慮すべきは「保存プロセスのブロッキング」である。VBAはシングルスレッドで動作するため、データベース接続のタイムアウトや遅延は、ユーザーの操作感を著しく損なう。そのため、接続のライフサイクル管理と、例外処理の徹底がアーキテクチャの肝となる。
—
2. データ・スキーマの定義
まず、SQL Server側に受け皿を用意する。単なるログではなく、ベースライン(Baseline 0-10)の状況も把握できる構造にする。
CREATE TABLE ProjectChangeLogs (
LogID INT IDENTITY(1,1) PRIMARY KEY,
ProjectName NVARCHAR(255),
FilePath NVARCHAR(1000),
LastSavedBy NVARCHAR(100),
MachineName NVARCHAR(100),
Baseline0Start DATETIME,
Baseline0Finish DATETIME,
Baseline0Cost MONEY,
SaveTimestamp DATETIME DEFAULT GETDATE(),
AppVersion NVARCHAR(50)
);
—
3. 実装:Windows APIとADODBの融合
Project VBAでイベントを捕捉するには、クラスモジュールによるイベントリスナーの実装が必須である。また、環境依存を排除するために、OSレベルの情報を取得するWindows APIを利用する。
クラスモジュール:`clsProjectEvents`
‘—————————————————————————————
‘ Module : clsProjectEvents
‘ Author : Legendary Architect
‘ Purpose : Projectのイベントをトラップし、DB連携トリガーを発火させる
‘—————————————————————————————
Option Explicit
Public WithEvents App As MSProject.Application
Private Sub App_ProjectBeforeSave(ByVal pj As Project, ByVal SaveAsCopy As Boolean, Cancel As Boolean)
On Error Resume Next
‘ 保存直前のスナップショットをDBへ記録
Call LoggingModule.WriteProjectLogToSQL(pj)
On Error GoTo 0
End Sub
標準モジュール:`LoggingModule`
ここで、ADODBを用いたSQL Serverへの高速な書き込みを実装する。ポイントは、「オブジェクトの明示的解放」と「パラメータクエリ」の使用だ。
Option Explicit
‘ Windows APIによる環境情報の取得
If VBA7 Then
Private Declare PtrSafe Function GetUserName Lib “advapi32.dll” Alias “GetUserNameA” (ByVal lpBuffer As String, nSize As Long) As Long
Private Declare PtrSafe Function GetComputerName Lib “kernel32” Alias “GetComputerNameA” (ByVal lpBuffer As String, nSize As Long) As Long
Else
Private Declare Function GetUserName Lib “advapi32.dll” Alias “GetUserNameA” (ByVal lpBuffer As String, nSize As Long) As Long
Private Declare Function GetComputerName Lib “kernel32” Alias “GetComputerNameA” (ByVal lpBuffer As String, nSize As Long) As Long
End If
‘ SQL Server接続文字列(環境に合わせて調整)
Private Const CONN_STRING As String = “Provider=MSOLEDBSQL;Data Source=SERVER_NAME;Initial Catalog=DB_NAME;Integrated Security=SSPI;”
Public Sub WriteProjectLogToSQL(ByVal pj As Project)
Dim conn As Object ‘ ADODB.Connection
Dim cmd As Object ‘ ADODB.Command
On Error GoTo ErrorHandler
‘ 1. オブジェクトのインスタンス化
Set conn = CreateObject(“ADODB.Connection”)
Set cmd = CreateObject(“ADODB.Command”)
‘ 2. 接続の確立(タイムアウト設定は短めにし、UIフリーズを防止)
conn.ConnectionTimeout = 5
conn.Open CONN_STRING
‘ 3. パラメータクエリの構築
‘ 文字列結合によるSQL構築はSQLインジェクションと実行計画の再利用性の観点から禁忌である。
With cmd
.ActiveConnection = conn
.CommandText = “INSERT INTO ProjectChangeLogs (ProjectName, FilePath, LastSavedBy, MachineName, Baseline0Start, Baseline0Finish, Baseline0Cost, AppVersion) ” & _
“VALUES (?, ?, ?, ?, ?, ?, ?, ?)”
.CommandType = 1 ‘ adCmdText
‘ パラメータの追加(順序が重要)
.Parameters.Append .CreateParameter(“@P1”, 202, 1, 255, pj.Name) ‘ adVarWChar
.Parameters.Append .CreateParameter(“@P2”, 202, 1, 1000, pj.FullName)
.Parameters.Append .CreateParameter(“@P3”, 202, 1, 100, GetCurrentUser())
.Parameters.Append .CreateParameter(“@P4”, 202, 1, 100, GetMachineName())
‘ ベースライン情報の処理(日付が未設定の場合はNullを許容)
.Parameters.Append .CreateParameter(“@P5”, 135, 1, , IIf(pj.Baseline0Start = “NA”, Null, pj.Baseline0Start)) ‘ adDBTimeStamp
.Parameters.Append .CreateParameter(“@P6”, 135, 1, , IIf(pj.Baseline0Finish = “NA”, Null, pj.Baseline0Finish))
.Parameters.Append .CreateParameter(“@P7”, 6, 1, , pj.Baseline0Cost) ‘ adCurrency
.Parameters.Append .CreateParameter(“@P8”, 202, 1, 50, Application.Version)
‘ 実行
.Execute
End With
CleanUp:
‘ 4. オブジェクトの厳密な解放(LIFO順)
If Not cmd Is Nothing Then Set cmd = Nothing
If Not conn Is Nothing Then
If conn.State = 1 Then conn.Close ‘ adStateOpen
Set conn = Nothing
End If
Exit Sub
ErrorHandler:
‘ ユーザーの作業を妨げないよう、エラーはサイレントにログ出力するか、
‘ 管理者への通知(Debug.Print等)に留める
Debug.Print “Log Write Error: ” & Err.Description
Resume CleanUp
End Sub
‘ — ヘルパー関数 —
Private Function GetCurrentUser() As String
Dim buffer As String 255
If GetUserName(buffer, 255) <> 0 Then
GetCurrentUser = Left(buffer, InStr(buffer, vbNullChar) – 1)
Else
GetCurrentUser = “Unknown”
End If
End Function
Private Function GetMachineName() As String
Dim buffer As String 255
If GetComputerName(buffer, 255) <> 0 Then
GetMachineName = Left(buffer, InStr(buffer, vbNullChar) – 1)
Else
GetMachineName = “Unknown”
End If
End Function
—
4. 運用の要諦:メモリ最適化とレガシーの罠
ライフサイクル管理
`clsProjectEvents` は、`ThisProject` モジュールやアドインのロード時に一度だけインスタンス化され、グローバル変数(または `Static`)として保持されなければならない。インスタンスが破棄されれば、イベントの捕捉も止まる。これはVBAエンジニアが最も犯しやすいミスの一つだ。
参照設定の最小化
上記のコードでは `CreateObject` による Late Binding(遅延結合)を採用している。これは、配布先の環境によって ADO (ActiveX Data Objects) のバージョンが異なることによる参照エラーを防ぐためだ。パフォーマンスに数ミリ秒の差は出るが、保守性を優先すべき領域である。
SQL Serverの負荷
全ユーザーが同時に保存を行う可能性のある環境では、SQL Server側でロック競合が発生しないよう、`NOLOCK` ヒントの活用や、ログテーブルのパーティショニングを検討せよ。また、`Integrated Security=SSPI` を用いることで、個別のDBアカウント管理という「セキュリティ上の負債」を回避できる。
—
5. 結論:データは沈黙しない
プロジェクトファイルが保存されるたびに、その背後でサイレントにデータベースが更新される。この仕組みが稼働し始めると、管理者は「どのプロジェクトが、いつ、誰によって、どのようにベースラインから乖離したか」を、プロジェクトファイルを開くことなく SQL 一発で可視化できるようになる。
VBAは古臭い技術だと言われる。しかし、Windows APIとSQL Serverを組み合わせ、正しくオブジェクトのライフサイクルを制御すれば、最新のSaaSにも劣らない強力なガバナンス・ツールへと変貌する。
コードの美しさは、その構文ではなく、如何に堅牢に、如何に静かにシステムを支え続けるかにある。貴殿の構築するログシステムが、組織の透明性を支える礎となることを願っている。
