【テクニカル・上級編】【上級プロフェッショナル向け】FSOファイル操作の「ロールバック(取り消し)機能」を備えた擬似トランザクションマネージャー – VBScript (Visual Basic Scripting Edition)解析バイブル

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

【極限の知見】VBScriptにおけるファイル操作の原子性を担保せよ:擬似トランザクションマネージャーの実装論

エンタープライズの最前線において、スクリプトによるファイル操作は常に「不完全性」というリスクを孕んでいる。

FileSystemObject (FSO) は、極めて直感的で強力なインターフェースを提供する。しかし、その設計思想には「原子性(Atomicity)」が欠落している。複数のファイルを作成、移動、削除する一連のプロセスにおいて、途中でネットワーク断線や権限エラー、ディスクフルが発生したとしよう。FSOはその時点で処理を停止するが、それまでに実行された操作を「無かったこと」にする機能は持ち合わせていない。

結果として、システムには「中途半端に作成されたファイル」や「消失したデータ」が残り、手動でのリカバリ、あるいはより大規模なシステム復旧作業という、エンジニアにとって最も忌むべきコストを強いることになる。

本稿では、この脆弱性を克服するため、VBScriptにおいてコマンドパターンを用いた「擬似トランザクションマネージャー」を実装する極限の設計手法を解説する。

1. アーキテクチャの設計思想:逆操作のスタック管理

トランザクションを実現するためには、実行したすべての操作に対して、それを完全に打ち消す「逆操作(Undo Action)」を定義し、それをLIFO(Last-In, First-Out)のスタック構造で保持する必要がある。

ここで重要なのは、単に「削除」を記録するのではないということだ。

逆操作の定義マトリクス

実行操作 (Action) 逆操作 (Inverse Action) 備考
CreateFile DeleteFile 作成したファイルを削除する
MoveFile MoveFile (Reverse) 元のパスへ移動し直す
DeleteFile RestoreFile 重要: 物理削除せず、一時領域へ退避させる
RenameFile RenameFile (Reverse) 元の名前に戻す

特に「Delete」の扱いは、シニアエンジニアとしての腕の見せ所だ。FSOの `DeleteFile` を直接呼び出してしまった後では、トランザクションは破綻する。我々のマネージャーは、削除要求を「物理的な消去」ではなく「隠蔽された一時ディレクトリへの移動」として解釈し、ロールバック時にのみ真の消去を行う。

2. 実装:FileSystemTransactionManager クラス

以下に、実戦投入に耐えうる設計思想に基づいたクラス実装を示す。このコードは、VBAおよびVBScript環境で動作するように設計されている。

' ==============================================================================
' Class: FileSystemTransactionManager
' Description: FSO操作の原子性を確保するための擬似トランザクションマネージャー
' Author: Legendary Chief Architect
' ==============================================================================
Class FileSystemTransactionManager
    Private FSO
    Private ActionStack ' 操作履歴を格納するコレクション
    Private TempStagingDir ' 削除ファイルの退避用ディレクトリ
    Private IsInTransaction ' トランザクション中フラグ

    ' --------------------------------------------------------------------------
    ' Constructor: 初期化
    ' --------------------------------------------------------------------------
    Private Sub Class_Initialize()
        Set FSO = CreateObject("Scripting.FileSystemObject")
        Set ActionStack = CreateObject("Scripting.Dictionary") ' 簡易スタックとして利用
        ' 実際には、操作順序を厳密に管理するため、Index管理が必要
        Set ActionStack = CreateObject("System.Collections.ArrayList") 
        
        ' 退避用ディレクトリの作成(%TEMP%配下)
        TempStagingDir = FSO.GetSpecialFolder(2) & "\VBS_TRANS_" & Timer()
        IsInTransaction = False
    End Sub

    ' --------------------------------------------------------------------------
    ' BeginTransaction: トランザクションの開始
    ' --------------------------------------------------------------------------
    Public Sub BeginTransaction()
        If IsInTransaction Then Err.Raise 1001, "TM", "Transaction already in progress."
        
        If Not FSO.FolderExists(TempStagingDir) Then
            FSO.CreateFolder(TempStagingDir)
        End If
        
        IsInTransaction = True
        ' スタックのクリア
        Do While ActionStack.Count > 0
            ActionStack.RemoveAt 0
        Loop
    End Sub

    ' --------------------------------------------------------------------------
    ' SafeDelete: 削除操作(物理削除せず退避)
    ' --------------------------------------------------------------------------
    Public Sub SafeDelete(ByVal targetPath)
        If Not IsInTransaction Then Err.Raise 1002, "TM", "No active transaction."
        
        Dim fileName, stagingPath
        fileName = FSO.GetFileName(targetPath)
        stagingPath = TempStagingDir & "\" & Timer() & "_" & fileName
        
        ' 実際の削除ではなく、退避を実行
        FSO.MoveFile targetPath, stagingPath
        
        ' 逆操作(復元)をスタックに積む
        ' パターン: [Type, OriginalPath, StagingPath]
        ActionStack.Add Array("DELETE", targetPath, stagingPath)
    End Sub

    ' --------------------------------------------------------------------------
    ' SafeMove: 移動操作
    ' --------------------------------------------------------------------------
    Public Sub SafeMove(ByVal source, ByVal dest)
        If Not IsInTransaction Then Err.Raise 1002, "TM", "No active transaction."
        
        FSO.MoveFile source, dest
        
        ' 逆操作(元の場所へ戻す)をスタックに積む
        ActionStack.Add Array("MOVE", source, dest)
    End Sub

    ' --------------------------------------------------------------------------
    ' Rollback: 全操作の取り消し
    ' --------------------------------------------------------------------------
    Public Sub Rollback()
        On Error Resume Next ' ロールバック中のエラーは致命的
        
        ' スタックを末尾(最新の操作)から順に処理
        Dim i, action
        For i = ActionStack.Count - 1 To 0 Step -1
            action = ActionStack(i)
            
            Select Case action(0)
                Case "DELETE"
                    ' 退避していたファイルを元の場所に戻す
                    FSO.MoveFile action(2), action(1)
                Case "MOVE"
                    ' 移動したファイルを元の場所に戻す
                    FSO.MoveFile action(2), action(1)
                Case "CREATE"
                    ' 作成したファイルを削除
                    FSO.DeleteFile action(1)
            End Select
        Next
        
        ' 後始末
        Cleanup
    End Sub

    ' --------------------------------------------------------------------------
    ' Commit: トランザクションの確定
    ' --------------------------------------------------------------------------
    Public Sub Commit()
        If Not IsInTransaction Then Exit Sub
        
        ' 確定時は、退避していた「削除済みファイル」を物理的に消去する
        Cleanup
        IsInTransaction = False
    End Sub

    ' --------------------------------------------------------------------------
    ' Private: リソースの解放とクリーンアップ
    ' --------------------------------------------------------------------------
    Private Sub Cleanup()
        ' 退避ディレクトリの物理削除
        If FSO.FolderExists(TempStagingDir) Then
            FSO.DeleteFolder TempStagingDir, True
        End If
        
        ' オブジェクトの明示的解放(メモリリーク防止)
        Set ActionStack = Nothing
        Set FSO = Nothing
        IsInTransaction = False
    End Sub

    ' Destructor
    Private Sub Class_Terminate()
        Cleanup
    End Sub
End Class

3. プロフェッショナルのための技術深掘り

3.1 メモリ管理とオブジェクトのライフサイクル

VBScriptにおける `Set obj = Nothing` は、単なる作法ではない。特に `FileSystemObject` や `Dictionary` などのCOMオブジェクトを多用する場合、明示的な解放を怠ると、スクリプト終了後もプロセスがゾンビ化したり、ファイルハンドルがロックされたままになるリスクがある。本実装では `Class_Terminate` を活用し、オブジェクトの破棄を確実に行っている。

3.2 Windows APIとの境界線

FSOは高レベルな抽象化を提供してくれるが、低レベルな制御(例:ファイルロックの強制解除や、NTFSのジャーナルへの直接アクセス)が必要な場合は、`Declare Function` を用いた `kernel32.dll` への呼び出しを検討すべきだ。しかし、本稿のようなトランザクション管理においては、FSOの抽象化を利用しつつ、「論理的な状態遷移」をコードで制御することに注力するのが、保守性の観点から最も賢明な判断となる。

3.3 パフォーマンスへの影響

このマネージャーは、すべての操作に対して「逆操作の記録」を行うため、通常のFSO操作に比べてI/Oオーバーヘッドが増大する。特に大量のファイルを扱う場合、`TempStagingDir` への移動コストを無視できない。
しかし、「データの整合性」という価値は、数秒の実行速度よりも遥かに重い。 実行速度を優先すべきバッチ処理と、整合性を優先すべきシステム操作を明確に使い分けること。これがアーキテクトの責務である。

4. 実践的な利用パターン

以下に、エラー発生時に自動的にロールバックを行う実装例を示す。

Dim tm
Set tm = New FileSystemTransactionManager

On Error Resume Next ' エラーハンドリングの開始

tm.BeginTransaction()

' --- シナリオ: ファイルの移動と一部削除のセット ---
tm.SafeMove "C:\Data\Source.txt", "C:\Data\Dest.txt"
tm.SafeDelete "C:\Data\OldLog.log"

' 意図的なエラー発生のシミュレーション
' もしここでエラーが起きれば、Dest.txtは元に戻り、OldLog.logは復元される
' Err.Raise 1 

If Err.Number <> 0 Then
    WScript.Echo "Error detected: " & Err.Description & ". Rolling back..."
    tm.Rollback()
Else
    tm.Commit()
    WScript.Echo "Transaction completed successfully."
End If

Set tm = Nothing

結語

VBScriptはレガシーな技術と見なされがちだが、その制約を知り尽くし、設計によって制約を凌駕するプロセスは、現代のエンジニアリングにおいても変わらぬ真理である。
「失敗したときにどうするか」を設計に組み込める者だけが、真に信頼されるシステムを構築できる。

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