Project VBAを掌握する極限の知見:Active Directory連携によるリソースメールアドレス自動同期のアーキテクチャ
大規模なプロジェクト管理において、リソースの連絡先情報の正確性は、プロジェクトの成否を握る隠れたクリティカルパスである。Microsoft Project(以下、MSP)のリソースプールにおける「電子メールアドレス」の陳腐化は、ステークホルダー間のコミュニケーションロス、タスクアサインの遅延、そして予期せぬプロジェクト停滞を引き起こす。
特に数千人規模の組織において、手動でのリソース情報更新は破綻を意味する。本稿では、Active Directory(以下、AD)の最新ディレクトリーサービスとMSPのリソースシートをVBA(Visual Basic for Applications)およびADSI(Active Directory Service Interfaces)を駆使して完全同期させる、極限の自動化アーキテクチャを解説する。
レガシーなVBAの限界を突破し、メモリリークを根絶し、システム間連携の信頼性を極限まで高めるための技術的知見をここに開示する。
—
1. アーキテクチャ設計思想:なぜVBAとADSIの融合なのか
プロジェクト管理システムとディレクトリサービスの連携において、通常であればC#やPowerShellを用いたモダンなバッチ処理が選択される。しかし、社内の限定された権限や、PMO(Project Management Office)が日常的に運用する「単一の`.mpp`ファイル、あるいはエンタープライズ環境(Project Server / Project Online)の手前におけるローカルリソースプール管理」という現実解において、VBAは依然として最強の即時性を誇る。
パフォーマンスとメモリの制約
MSPのオブジェクトモデルは、COM(Component Object Model)の wrappers 上に構築されており、不適切なオブジェクト参照の保持は即座にメモリリークやCOM例外(`0x8001010A` – サーバーが忙しすぎます)を引き起こす。
ADSIをVBAから直接叩く場合、`ActiveXObject` や `ADODB.Connection` を経由するため、「取得したCOMオブジェクトのスコープ管理」と「明示的な参照解放(`Set obj = Nothing`)」の徹底がシステムの寿命を決定づける。
—
2. 実装コード:AD・MSP高速同期エンジン
以下のコードは、LDAP(Lightweight Directory Access Protocol)経由でADから全ユーザーの「`sAMAccountName`(または社員番号等)」と「`mail`」属性を一括取得し、MSPのリソースシート(`Resource`オブジェクト群)と高速に突合・更新するプロダクションクオリティの実装である。
Option Explicit
‘ —————————————————————–
‘ @Title: Active Directory 連携リソースメールアドレス同期エンジン
‘ @Architecture: ADSI (LDAP) + MS Project Object Model
‘ @Description: 組織のADから最新のメールアドレスを取得し、MSPのリソースへ非同期的に反映する。
‘ —————————————————————–
Public Sub SyncResourceEmailFromAD()
Dim prj As Project
Set prj = ActiveProject
‘ 実行時のパフォーマンス最大化(画面描画・イベントの抑制)
With Application
.ScreenUpdating = False
.DisplayAlerts = False
.Calculation = pjManual
End With
On Error GoTo ErrorHandler
Debug.Print “=== AD同期プロセス開始: ” & Now & ” ===”
‘ 1. ADからメールアドレス辞書(Dictionary)を一括取得
Dim dictMailMap As Object
Set dictMailMap = GetActiveDirectoryEmailMap()
If dictMailMap.Count = 0 Then
MsgBox “ADから有効なユーザー情報を取得できませんでした。ネットワークまたはLDAPパスを確認してください。”, vbCritical, “同期エラー”
GoTo Finally
End If
‘ 2. MSPリソースプールの走査と更新
Dim res As Resource
Dim updateCount As Long
Dim targetKey As String
updateCount = 0
For Each res In prj.Resources
‘ リソースが無効(Null)またはプレースホルダー、コストリソースの場合はスキップ
If Not res Is Nothing Then
If res.Type <> pjResourceTypeCost And res.Name <> “” Then
‘ キーの選定: リソースの「略称(Initials)」または「電子メールアドレスのプレフィックス」をADのID(例: sAMAccountName / employeeID)とマッピングする前提
‘ ここではリソースの「ID」または「名前/Windowsアカウント」をキーとして利用する設計とする
‘ 実運用ではリソースのカスタムフィールドや「Windows アカウント (ResourceNTAccount)」をキーに推奨
targetKey = Trim$(res.WindowsAccount)
‘ ドメイン名が含まれている場合(DOMAIN\username)のパース処理
If InStr(targetKey, “\”) > 0 Then
targetKey = Split(targetKey, “\”)(1)
End If
If targetKey <> “” Then
If dictMailMap.Exists(LCase$(targetKey)) Then
Dim newEmail As String
newEmail = dictMailMap(LCase$(targetKey))
‘ 既存のメールアドレスと異なる場合のみ更新(無駄なプロパティ書き込みを排除)
If res.EmailAddress <> newEmail Then
res.EmailAddress = newEmail
updateCount = updateCount + 1
Debug.Print “更新: ” & res.Name & ” -> ” & newEmail
End If
End If
End If
End If
End If
Next res
Debug.Print “=== AD同期プロセス完了. 更新件数: ” & updateCount & ” ===”
MsgBox “Active Directoryとの同期が完了しました。” & vbCrLf & “更新されたリソース数: ” & updateCount, vbInformation, “同期成功”
Finally:
‘ 3. 確実なリソース解放(メモリリーク防止)
Set dictMailMap = Nothing
‘ アプリケーション設定の復元
With Application
.ScreenUpdating = True
.DisplayAlerts = True
.Calculation = pjAutomatic
End With
Exit Sub
ErrorHandler:
MsgBox “予期せぬエラーが発生しました。” & vbCrLf & _
“Error Number: ” & Err.Number & vbCrLf & _
“Description: ” & Err.Description, vbCritical, “致命的エラー”
Resume Finally
End Sub
‘ —————————————————————–
‘ ADSIを用いたLDAPクエリの発行とメモリ最適化
‘ —————————————————————–
Private Function GetActiveDirectoryEmailMap() As Object
Dim objRootDSE As Object
Dim objConnection As Object
Dim objCommand As Object
Dim objRecordSet As Object
Dim dict As Object
Set dict = CreateObject(“Scripting.Dictionary”)
dict.CompareMode = vbTextCompare ‘ 大文字小文字を区別しない
On Error GoTo ADError
‘ 接続プールの確立
Set objConnection = CreateObject(“ADODB.Connection”)
objConnection.Provider = “ADsDSOObject”
objConnection.Open “Active Directory Provider”
Set objCommand = CreateObject(“ADODB.Command”)
Set objCommand.ActiveConnection = objConnection
‘ 現在のドメインコンテキストを動的に取得(ハードコーディングを回避する極意)
Set objRootDSE = GetObject(“LDAP://RootDSE”)
Dim strDefaultNamingContext As String
strDefaultNamingContext = objRootDSE.Get(“defaultNamingContext”)
‘ LDAPクエリの構築: 有効なユーザーアカウントかつメールアドレスが存在するものに限定
‘ パフォーマンスチューニングのため、必要な列(sAMAccountName, mail)のみを射影
objCommand.CommandText = _
“
“(&(objectCategory=person)(objectClass=user)(mail=)(!(userAccountControl:1.2.840.113556.1.4.803:=2)));” & _
“sAMAccountName,mail;subtree”
objCommand.Properties(“Page Size”) = 1000
objCommand.Properties(“Timeout”) = 30
objCommand.Properties(“Cache Results”) = False ‘ 大規模組織でのメモリ圧迫を防ぐため結果のローカルキャッシュを無効化
Set objRecordSet = objCommand.Execute
Do Until objRecordset.EOF
Dim samAccName As String
Dim mailAddr As String
If Not IsNull(objRecordSet.Fields(“sAMAccountName”).Value) And _
Not IsNull(objRecordSet.Fields(“mail”).Value) Then
samAccName = CStr(objRecordSet.Fields(“sAMAccountName”).Value)
mailAddr = CStr(objRecordSet.Fields(“mail”).Value)
If Not dict.Exists(samAccName) Then
dict.Add LCase$(samAccName), mailAddr
End If
End If
objRecordset.MoveNext
Loop
ADCleanUp:
‘ オブジェクトの明示的破棄(逆順での解放が安全)
On Error Resume Next
If Not objRecordSet Is Nothing Then
If objRecordSet.State Then objRecordSet.Close
Set objRecordSet = Nothing
End If
Set objCommand = Nothing
If Not objConnection Is Nothing Then
If objConnection.State Then objConnection.Close
Set objConnection = Nothing
End If
Set objRootDSE = Nothing
Set GetActiveDirectoryEmailMap = dict
Exit Function
ADError:
Debug.Print “AD接続エラー: ” & Err.Number & ” – ” & Err.Description
Resume ADCleanUp
End Function
—
3. チーフアーキテクトが指摘する「実装上の急所」
上記のコードを実務の現場に投入するにあたり、通常のプログラミング解説では語られない「致命的な罠と回避策」を共有する。
A. メモリリークの温床となる `ADODB.Recordset` の解放順序
VBAにおけるCOMオブジェクトの寿命管理は極めてデリケートである。特に `ADODB.Connection` と `Recordset` は、`Close` メソッドを明示的に呼び出した上で `Set obj = Nothing` を行わなければ、VBAのプロセスが終了してもメモリ上にハンドルが残存する(ゾンビプロセス化)。
さらに、`Cache Results = False` を指定することで、数万件規模のADユーザー情報を取得した際のヒープ領域の肥大化を物理的に阻止している。
B. `ScreenUpdating` と `Calculation` の制御による爆発的高速化
MSPのリソースを何千件も走査する際、デフォルトのままプロパティ書き込み(`res.EmailAddress = …`)を行うと、その都度MSP内部のUI描画エンジンと依存関係の再計算トリガーが引かれ、処理が数分単位でフリーズする。
前処理としての `ScreenUpdating = False` および `Calculation = pjManual` の明示的な設定は、処理速度を数十倍から数百倍に跳ね上げる必須の儀式である。
C. ドメイン名の揺らぎ(NetBIOS名 vs UPN)への対策
AD環境において、Windowsアカウントは `DOMAIN\username` や `username@domain.local`(UPN)など、入力元によって形式がバラバラになる傾向がある。
本コードでは `Split(targetKey, “\”)(1)` や `LCase$` による正規化を挟むことで、リソースシート側の記述揺れによるマッチング漏れを構造的に根絶している。
—
4. レガシーエンタープライズ環境への適用と保守戦略
組織のセキュリティポリシー変更やインフラストラクチャのクラウド移行(Azure AD / Microsoft Entra ID へのシフト)を見据えた場合、純粋なLDAP(オンプレミスAD)直叩きのアプローチはいずれ限界を迎える。
しかし、Microsoft Graph APIへの完全移行が完了するまでの過渡期において、このVBA + ADSIアーキテクチャは「追加のミドルウェアやランタイムインストールを一切必要とせず、クライアントPC単体で完結する」という圧倒的な可用性を持つ。
大規模PMOの現場において、インフラの変更に左右されず、エクセルやプロジェクトファイル単体で強靭な自動化基盤を維持し続けること。それこそが、レガシーとモダンを知り尽くしたエンジニアが構築すべき「真に持続可能なシステム」の姿である。
