【上級者向け】SQL Serverのトランザクションログと連携したProject VBAプロジェクト保存のロールバック処理
VBAと外部データベースを同期させるシステムにおいて、最も悪名高い問題は「ファイルの保存成功」と「RDBのコミット成功」の非アトミック性(不可分性の欠如)である。
MS Projectのローカルファイル(`.mpp`)またはサーバー上のプロジェクトデータ保存と、SQL Server側の業務トランザクション(進捗実績、コスト、リソース割当等)を完全に同期させなければならない現場は多い。しかし、VBAの `Project.Save` メソッドが成功した後にSQL Serverへの書き込みがコケた場合、あるいはその逆が発生した場合、システムは致命的なデータ不整合(孤立データ、あるいは「存在しないタスクのコスト実績」)の闇に突入する。
今回は、Project VBAの実行コンテキストからSQL Serverのトランザクションログを直接制御し、分散トランザクションに近い堅牢性を持たせた「極限のロールバック処理アーキテクチャ」を解説する。
—
1. アーキテクチャの核心:なぜ通常のVBAエラーハンドリングでは破綻するのか?
一般的なVBAのコードは、`On Error GoTo` による局所的なジャンプを行う。しかし、COMオブジェクトとしてのMS Projectと、ADO(ActiveX Data Objects)を通じたSQL Serverの接続は、メモリ空間もライフサイクルも異なる。
- Projectの保存: ファイルI/Oを伴い、失敗時はCOM例外を投げるが、部分的なファイル書き込みによる破損リスクを抱える。
- SQL Serverの更新: トランザクションログ(`BEGIN TRANSACTION`)の管理下にあるが、VBA側から切断(`Connection.Close`)や予期せぬクラッシュが起きると、暗黙のロールバックが発生するものの、MS Project側のファイル変更はそのまま残る。
この「片肺飛行」を防ぐためには、SQL Server側で明示的なトランザクションを張った上で、VBA側でセーブポイント(Savepoint)をエミュレートし、すべての処理が成功したときのみ `COMMIT`、一つでも例外を検知したら逆順で厳密な原状回復(ロールバック)を行う必要がある。
—
2. 実装コード:トランザクション同期マネージャー
以下のコードは、MS Projectの保存処理と、SQL Serverへの一括書き込みを単一のトランザクション境界として保護する実用的なクラスモジュール(あるいは標準モジュール)の極限形である。
ここでは、メモリリークを完全に排除するため、ADOオブジェクトのライフサイクルを厳密に管理し、確実な解放を行っている。
‘ ==============================================================================
‘ モジュール名: clsProjectTransactionManager
‘ 概要: MS Projectの保存とSQL Serverトランザクションの完全同期ロールバック制御
‘ ==============================================================================
Option Explicit
‘ SQL Server接続文字列(環境に合わせて変更)
Private Const CONNECTION_STRING As String = “Provider=SQLOLEDB;Server=127.0.0.1;Database=ProjectDB;Uid=sa;Pwd=YourPassword;”
Public Sub ExecuteSynchronizedSave(ByلTargetProject As Project, ByVal targetFilePath As String)
Dim conn As Object ‘ ADODB.Connection
Dim cmd As Object ‘ ADODB.Command
Dim isSqlTranStarted As Boolean
Dim isProjectSaved As Boolean
isSqlTranStarted = False
isProjectSaved = False
‘ 1. ADOコネクションの初期化とオープン(メモリ最適化のため遅延バインディングを採用)
Set conn = CreateObject(“ADODB.Connection”)
conn.ConnectionString = CONNECTION_STRING
conn.CommandTimeout = 30
conn.ConnectionTimeout = 15
conn.Open
‘ 2. SQL Server側のトランザクション開始
conn.BeginTrans
isSqlTranStarted = True
On Error GoTo ErrorHandler
‘ 3. 【フェーズA】SQL Server側へメタデータ・進捗実績の事前書き込み(仮登録)
Set cmd = CreateObject(“ADODB.Command”)
Set cmd.ActiveConnection = conn
cmd.CommandType = 1 ‘ adCmdText
‘ 例: プロジェクトの最終同期日時とステータスを一時更新
cmd.CommandText = “UPDATE ProjectMeta SET LastSyncStatus = ‘SYNCING’ WHERE ProjectID = ?”
cmd.Parameters.Append cmd.CreateParameter(“p1”, 200, 1, 50, TargetProject.Name) ‘ adVarChar
cmd.Execute
‘ 4. 【フェーズB】MS Project自体のファイル保存実行
‘ ※ここでI/Oエラーやディスク容量不足等のCOM例外が発生しうる
TargetProject.SaveAs FileName:=targetFilePath
isProjectSaved = True
‘ 5. 【フェーズC】SQL Server側の本番コミット処理(ファイル保存成功を前提とする)
cmd.CommandText = “UPDATE ProjectMeta SET LastSyncStatus = ‘SUCCESS’, FilePath = ? WHERE ProjectID = ?”
‘ パラメータの再利用または再構築
Set cmd = Nothing
Set cmd = CreateObject(“ADODB.Command”)
Set cmd.ActiveConnection = conn
cmd.CommandText = “UPDATE ProjectMeta SET LastSyncStatus = ‘SUCCESS’, FilePath = ? WHERE ProjectID = ?”
cmd.Parameters.Append cmd.CreateParameter(“p1”, 200, 1, 255, targetFilePath)
cmd.Parameters.Append cmd.CreateParameter(“p2”, 200, 1, 50, TargetProject.Name)
cmd.Execute
‘ すべて成功:SQLトランザクションを確定
conn.CommitTrans
isSqlTranStarted = False
MsgBox “プロジェクトの保存とDBトランザクションの同期に成功しました。”, vbInformation, “同期完了”
GoTo CleanUp
ErrorHandler:
‘ 致命的なエラーの捕捉
Dim errDesc As String
errDesc = Err.Description
‘ 【ロールバック処理】
‘ SQL Server側トランザクションの巻き戻し
If isSqlTranStarted Then
On Error Resume Next
conn.RollbackTrans
isSqlTranStarted = False
On Error GoTo 0
End If
‘ MS Project側のファイル保存がすでに完了してしまっている場合の逆補正
‘ (必要に応じて、バックアップからの復元ロジックやフラグクリアをここに記述)
If isProjectSaved Then
‘ 警告: ファイルは保存されたがDBがロールバックされた旨をログ出力・通知
Debug.Print “[CRITICAL WARNING] Project file was saved, but SQL Server transaction was rolled back.”
End If
MsgBox “エラーが発生したため、処理をロールバックしました。” & vbCrLf & _
“詳細: ” & errDesc, vbCritical, “トランザクション異常終了”
CleanUp:
‘ オブジェクトの明示的解放(メモリリークの完全防止)
On Error Resume Next
If Not cmd Is Nothing Then Set cmd = Nothing
If Not conn Is Nothing Then
If conn.State = 1 Then conn.Close ‘ adStateOpen = 1
Set conn = Nothing
End If
On Error GoTo 0
End Sub
—
3. チーフアーキテクトが指摘する「見落とされがちな罠」と最適化の極意
オブジェクトのライフサイクルとメモリ管理
VBAのガベージコレクションは非決定的(Deterministicではない)である。特にADOオブジェクト(`Connection`, `Command`, `Recordset`)をループ内や複雑なエラーパスで解放し忘れると、COMコンポーネントの参照カウントが残存し、ExcelやProjectプロセスがメモリ上にゾンビとして居座る。
上記のコードでは、`CleanUp` ラベルを確実に通過させ、`Set obj = Nothing` の前に `Close` メソッドを明示的に呼び出すことで、ハンドルリークをゼロに抑え込んでいる。
ネットワーク切断・タイムアウトへの備え
SQL Serverとの通信中にVPNが瞬断したり、DBがロック競合を起こした場合、VBAは無応答(フリーズ)状態になりやすい。
`CommandTimeout` および `ConnectionTimeout` を明示的にコード内で設定しているのはそのためだ。デフォルト値(通常は無制限または非常に長い)に依存するコードは、レガシーシステムにおいて悪の根源となる。必ずタイムアウト値を秒単位でハードコーディング、あるいはコンフィグから読み込ませること。
ファイルI/OとDBトランザクションの順序性
「DBを先に更新するか、ファイルを先に保存するか」はアーキテクトの哲学が分かれるところだが、「非可逆なファイル保存を後に行い、ファイル保存が成功した瞬間をコミットのトリガーとする」のが、MS Projectアドオン開発における黄金律である。
なぜなら、ローカルファイルや共有フォルダ上の `.mpp` ファイルは、OSのファイルロックや権限問題など、DBよりも外部要因による失敗確率が圧倒的に高いためだ。先にDBのトランザクションを重く保持したままファイル保存でモタつくと、SQL Server側のロックホルダーを圧迫し、他の業務システム全体を巻き込んだデッドロックを引き起こす。
—
結びにかえて
VBAは「おもちゃの言語」と揶揄されることがある。しかし、それは書く人間の技量が低い場合の話に過ぎない。Windows APIの裏側、COMの参照モデル、そしてRDBのトランザクションログの挙動を完全に脳内にマッピングできた者にとって、VBAは極めてダイレクトにOSリソースを叩ける強力な武器となる。
現場のシステムを保全するシニアエンジニアよ、甘いエラーハンドリングを捨てよ。すべてのレイヤーで整合性を担保するコードだけが、夜間バッチの恐怖からあなたを解放するのだ。
