【テクニカル・上級編】ADOでSQL Serverのバイナリデータ(画像・PDF)を読み書きする:Streamオブジェクトの活用法 – Access VBA解析バイブル

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

バイナリ・ストリームの極意:ADODB.StreamによるSQL Server BLOB操作の真髄

業務システムの成熟に伴い、データ構造は必然的に複雑化する。テキストデータ、数値データ、そして「非構造化データ」——画像、PDF、CADデータといったバイナリ(BLOB)の蓄積だ。

多くの開発者は、これらのバイナリデータを `Variant` 型の `Byte()` 配列として扱おうとする。しかし、それは極めて危うい思考だ。数MBを超えるファイルが頻繁にやり取りされる環境において、VBAのヒープメモリに巨大な配列を直接展開する行為は、メモリ断片化(Memory Fragmentation)を招き、最終的には `Out of Memory` エラーという形でシステムを破綻させる。

我々プロフェッショナルが取るべき道は、メモリを「塊」として扱うのではなく、「流れ(Stream)」として制御することだ。本稿では、`ADODB.Stream` オブジェクトを駆使し、SQL Server上のバイナリデータを、メモリ負荷を最小限に抑えつつ、堅牢に制御するためのアーキテクチャを詳解する。

1. なぜ `Byte()` 配列ではなく `ADODB.Stream` なのか

Access VBAにおいて、DAO(Data Access Objects)はローカルなJet/ACEエンジンに対しては強力だが、SQL ServerのようなリモートDBとの高度なバイナリ通信においては、ADO(ActiveX Data Objects)が不可欠となる。

ここで重要なのは、`Field.Value` を直接 `Byte()` 変数に代入する設計を避けることだ。

メモリ管理におけるリスク

1. 連続領域の確保失敗: `Byte()` 配列はメモリ上の連続した領域を要求する。大きなファイルを扱う際、メモリの空き容量は十分でも、断片化によって「連続した領域」が確保できず、エラーが発生する。
2. コピーコスト: 配列への代入は、データの物理的なコピーを発生させる。大規模なループ内でこれを行うと、CPUサイクルとメモリ帯域を無駄に消費する。

`ADODB.Stream` は、内部的にバッファリング機構を持っており、ファイルシステムとメモリ間の橋渡しを抽象化してくれる。これにより、我々は「データの塊」ではなく「データの流れ」を制御することに専念できるのだ。

2. 実装:DBからファイルへの書き出し(Read)

まずは、SQL Serverの `VARBINARY(MAX)` 型に格納されたデータを、ローカルファイルとして保存するプロセスだ。ここでは、レコードセットのカーソル位置と、Streamオブジェクトのライフサイクルに細心の注意を払う。

''
' SQL ServerのBLOBデータをファイルとしてエクスポートする
' @param connectionString  接続文字列
' @param recordsetID       対象レコードを特定するID
' @param targetFilePath    出力先のフルパス
' @return                  成功した場合はTrue
''
Public Function ExportBlobToFile(ByVal connectionString As String, _
                                ByVal recordsetID As Long, _
                                ByVal targetFilePath As String) As Boolean
    
    Dim conn As ADODB.Connection
    Dim rs As ADODB.Recordset
    Dim stm As ADODB.Stream
    Dim sql As String
    
    ExportBlobToFile = False
    
    ' エラーハンドリングの構築
    On Error GoTo ErrorHandler

    ' 1. Connectionの初期化
    Set conn = New ADODB.Connection
    conn.Open connectionString

    ' 2. Recordsetの取得
    ' 読み取り専用、前方専用カーソルを使用することでオーバーヘッドを最小化する
    sql = "SELECT ImageData FROM DocumentTable WHERE DocID = " & recordsetID
    Set rs = New ADODB.Recordset
    rs.Open sql, conn, adOpenForwardOnly, adLockReadOnly

    If Not rs.EOF Then
        ' 3. Streamオブジェクトの準備
        Set stm = New ADODB.Stream
        
        ' バイナリモードを指定
        stm.Type = adTypeBinary
        ' 書き込みモード(ファイルへの保存)
        stm.Open

        ' 4. データの転送
        ' Field.Valueを直接Streamに流し込む。
        ' ここで巨大なByte配列を介さず、ADOの内部機構を利用する。
        If Not IsNull(rs.Fields("ImageData").Value) Then
            stm.Write rs.Fields("ImageData").Value
            
            ' 5. ファイルへの保存
            ' adSaveCreateOverWrite を指定し、既存ファイルを上書きする
            stm.SaveToFile targetFilePath, adSaveCreateOverWrite
            ExportBlobToFile = True
        End If
        
        stm.Close
    End If

CleanUp:
    ' 6. ライフサイクルの厳格な管理(重要)
    ' COMオブジェクトは明示的にNothingを代入し、参照カウンタをゼロにする
    On Error Resume Next
    If Not stm Is Nothing Then Set stm = Nothing
    If Not rs Is Nothing Then
        If rs.State = adStateOpen Then rs.Close
        Set rs = Nothing
    End If
    If Not conn Is Nothing Then
        If conn.State = adStateOpen Then conn.Close
        Set conn = Nothing
    End If
    Exit Function

ErrorHandler:
    ' 実務環境では、ここでログ出力やWindowsイベントログへの記録を行う
    Debug.Print "Error: " & Err.Number & " - " & Err.Description
    Resume CleanUp
End Function

3. 実装:ファイルからDBへの書き込み(Write)

次に、逆のプロセス、すなわちローカルのファイル(PDFや画像)をSQL Serverの `VARBINARY(MAX)` へアップロードする処理だ。ここでは、ファイルの読み込み時に「バイナリモード」であることを徹底しなければならない。

''
' ローカルファイルをSQL ServerのBLOBへアップロードする
' @param connectionString  接続文字列
' @param recordsetID       対象レコードを特定するID
' @param sourceFilePath    読み込み元のファイルパス
''
Public Sub ImportFileToBlob(ByVal connectionString As String, _
                            ByVal recordsetID As Long, _
                            ByVal sourceFilePath As String)
    
    Dim conn As ADODB.Connection
    Dim rs As ADODB.Recordset
    Dim stm As ADODB.Stream
    Dim sql As String
    
    On Error GoTo ErrorHandler

    ' 1. ファイルの存在確認
    If Dir(sourceFilePath) = "" Then
        Err.Raise vbObjectError + 513, , "指定されたファイルが見つかりません。"
    End If

    ' 2. Connectionの初期化
    Set conn = New ADODB.Connection
    conn.Open connectionString

    ' 3. Streamオブジェクトによるファイル読み込み
    Set stm = New ADODB.Stream
    stm.Type = adTypeBinary
    stm.Open
    stm.LoadFromFile sourceFilePath ' ファイルをメモリバッファへロード

    ' 4. Recordsetの準備(更新可能なカーソルが必要)
    sql = "SELECT ImageData FROM DocumentTable WHERE DocID = " & recordsetID
    Set rs = New ADODB.Recordset
    ' 更新を行うため、adLockOptimistic を使用
    rs.Open sql, conn, adOpenStatic, adLockOptimistic

    If Not rs.EOF Then
        ' 5. StreamのデータをFieldに書き込む
        ' stm.Read でバッファの内容を読み出し、Fieldへ投入する
        rs.Fields("ImageData").Value = stm.Read
        rs.Update
    Else
        Err.Raise vbObjectError + 514, , "対象のレコードが存在しません。"
    End If

    stm.Close

CleanUp:
    On Error Resume Next
    If Not stm Is Nothing Then Set stm = Nothing
    If Not rs Is Nothing Then
        If rs.State = adStateOpen Then rs.Close
        Set rs = Nothing
    End If
    If Not conn Is Nothing Then
        If conn.State = adStateOpen Then conn.Close
        Set conn = Nothing
    End If
    Exit Sub

ErrorHandler:
    MsgBox "致命的なエラーが発生しました: " & Err.Description, vbCritical
    Resume CleanUp
End Sub

4. アーキテクトの視点:極限の安定性を担保するために

この実装を実務レベルの「製品」に昇華させるためには、以下の3点を意識せよ。

I. トランザクションの境界線

バイナリデータの更新は、ファイルシステムとデータベースという2つの異なるリソースを扱う。もし、DBの更新には成功したが、後の処理で失敗した場合、データの整合性が崩れる。
大量のファイルを一括処理する場合は、`conn.BeginTrans` と `conn.CommitTrans` を適切に配置し、エラー時には `conn.RollbackTrans` を確実に実行する設計にせよ。

II. カーソルタイプの選択

`adOpenForwardOnly` は、読み取り専用のシーケンシャルな走査において最も高速で、メモリ消費も少ない。大量のアーカイブデータをエクスポートするバッチ処理では、この選択がパフォーマンスを左右する。一方で、単一レコードの更新には `adOpenStatic` が必要となる。目的に応じた使い分けが、エンジニアの腕の見せ所だ。

III. オブジェクトの破棄(Explicit Release)

VBAはCOM(Component Object Model)に基づいている。`Set obj = Nothing` は単なる作法ではない。参照カウンタを即座に減らし、OSに対してリソースを返却するための「命令」である。特に `ADODB.Stream` のような、アンマネージドなリソース(ファイルハンドル等)を背後で保持するオブジェクトについては、スコープを抜ける前に確実に解放すること。これが、数ヶ月間稼働し続けるサーバーサイド・アプリケーションにおいて、メモリリークを防ぐ唯一の手段である。

結論

`ADODB.Stream` を使いこなすことは、単に「ファイルが扱える」ことではない。それは、データの物理的な実体と、メモリ上の論理的な表現の間の「境界」を、エンジニアが完全に掌握することを意味する。

大規模なレガシーシステムを、モダンで堅牢なアーキテクチャへと進化させるために。この知見を、君のコードに刻んでほしい。

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