【テクニカル・上級編】ADOでBLOBデータを扱う:SQL Server上の画像やPDFをVBAで入出力する – Access VBA解析バイブル

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

BLOBデータの真髄:ADOとStreamオブジェクトによるバイナリ操作の極致

大規模な業務システムにおいて、画像、PDF、あるいは設計図面といったバイナリデータ(BLOB: Binary Large Object)の扱いは、システムの成否を分ける分岐点となる。

多くの開発者は、`Recordset.Fields(“FileData”).Value` という単純な代入でこの問題を解決しようとする。しかし、それは小規模なプロトタイプにおける「無知による幸運」に過ぎない。ファイルサイズが数MBを超え、トランザクションが重なり、メモリ消費が累積する実戦の環境下では、そのアプローチは即座に `Run-time error ‘3020’: EOF` や、最悪の場合、Accessプロセス自体のメモリ枯渇によるクラッシュを招く。

本稿では、SQL Server上の `varbinary(max)` データを、いかにしてメモリ効率を最適化しつつ、堅牢にVBAから制御するか。その設計思想と実装の極致を伝授する。

1. アーキテクチャの核心:なぜRecordsetの直接代入では足りないのか

DAO(Data Access Objects)がローカルなファイルベースのエンジンの操作に特化しているのに対し、ADO(ActiveX Data Objects)はネットワーク越し、あるいはSQL Serverのような強力なRDBMSとの通信を前提としている。

BLOBデータを扱う際、最大の敵は「不必要なメモリコピー」と「オブジェクトのライフサイクル管理の放棄」である。

単純な代入を行うと、バイナリデータは一度、VBAのVariant型を経由してメモリ上に展開される。数MBのデータを数件扱うだけで、Accessの32bitプロセス(現代でもレガシー保守では現役だ)が持つメモリ空間は瞬く間に圧迫される。

これを回避する唯一の解が、`ADODB.Stream` オブジェクトの活用である。Streamオブジェクトは、メモリ上のバッファを直接ファイルシステムやバイナリデータへ橋渡しする「パイプライン」として機能する。

2. 実装:SQL Serverからファイルを抽出する(Read)

まずは、データベースからバイナリデータを読み取り、ローカルファイルとして保存する実装だ。ここでは、`CursorLocation` の設定が極めて重要になる。

''' <summary>
''' SQL Server上のBLOBデータをファイルとして出力する
''' </summary>
''' <param name="connectionString">接続文字列</param>
''' <param name="recordID">対象レコードのID</param>
''' <param name="savePath">保存先フルパス</param>
Public Sub ExportBlobToFile(ByVal connectionString As String, ByVal recordID As Long, ByVal savePath As String)
    Dim conn As ADODB.Connection
    Dim rs As ADODB.Recordset
    Dim stream As ADODB.Stream
    Dim sql As String

    On Error GoTo ErrorHandler

    ' 1. Connectionの初期化
    Set conn = New ADODB.Connection
    conn.ConnectionString = connectionString
    conn.CursorLocation = adUseClient ' クライアントサイドカーソルを指定
    conn.Open

    ' 2. SQLの構築(SELECT文で対象を絞り込む)
    sql = "SELECT FileData, FileName FROM DocumentTable WHERE ID = " & recordID

    ' 3. Recordsetの取得
    Set rs = New ADODB.Recordset
    rs.Open sql, conn, adOpenStatic, adLockReadOnly

    If Not rs.EOF Then
        ' 4. ADODB.Streamの構築
        ' TypeをadTypeBinaryに設定することが、バイナリ操作の鉄則である
        Set stream = New ADODB.Stream
        stream.Type = adTypeBinary
        stream.Open

        ' 5. フィールドのバイナリデータをStreamに書き込む
        ' rs.Fields("FileData").Value を直接使うのではなく、
        ' StreamのWriteメソッドを利用することでメモリ管理を最適化する
        stream.Write rs.Fields("FileData").GetChunk(-1) ' -1は全データを取得することを意味する

        ' 6. ファイルシステムへ書き出し
        stream.SaveToFile savePath, adSaveCreateOverWrite
        
        ' 7. 後始末(Streamのクローズ)
        stream.Close
        Set stream = Nothing
        
        Debug.Print "Export Successful: " & savePath
    Else
        Err.Raise vbObjectError + 513, "ExportBlobToFile", "指定されたレコードが見つかりません。"
    End If

CleanUp:
    ' 8. オブジェクトの明示的解放(これがメモリリークを防ぐ唯一の手段である)
    On Error Resume Next
    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 "Critical Error in ExportBlobToFile: " & Err.Description, vbCritical
    Resume CleanUp
End Sub

アーキテクトの視点:`GetChunk(-1)` の重み

`rs.Fields(“FileData”).Value` を使うのではなく、`GetChunk(-1)` を使用している点に注目せよ。これは、Recordsetのバッファから直接バイナリの塊をストリームへ流し込むための、より低レイヤーに近いアプローチである。

3. 実装:ファイルをデータベースへ格納する(Write)

次に、ローカルのファイルを読み込み、SQL Serverの `varbinary(max)` カラムへ流し込む処理だ。ここでは、ファイルの読み込みを `ADODB.Stream` で行い、そのストリーム自体をフィールドに代入する手法を取る。

''' <summary>
''' ローカルファイルをSQL Server上のBLOBカラムに保存する
''' </summary>
''' <param name="connectionString">接続文字列</param>
''' <param name="recordID">更新対象のレコードID</param>
''' <param name="filePath">読み込み元ファイルパス</param>
Public Sub ImportFileToBlob(ByVal connectionString As String, ByVal recordID As Long, ByVal filePath As String)
    Dim conn As ADODB.Connection
    Dim rs As ADODB.Recordset
    Dim stream As ADODB.Stream
    Dim sql As String

    On Error GoTo ErrorHandler

    ' ファイルの存在確認(基本中の基本だが、これを怠るエンジニアが多すぎる)
    If Dir(filePath) = "" Then
        Err.Raise vbObjectError + 514, "ImportFileToBlob", "指定されたファイルが存在しません。"
    End If

    Set conn = New ADODB.Connection
    conn.ConnectionString = connectionString
    conn.Open

    ' 1. 更新用Recordsetの展開(Optimistic Lockを使用)
    sql = "SELECT FileData FROM DocumentTable WHERE ID = " & recordID
    Set rs = New ADODB.Recordset
    rs.Open sql, conn, adOpenKeyset, adLockOptimistic

    If Not rs.EOF Then
        ' 2. Streamオブジェクトによるファイルの読み込み
        Set stream = New ADODB.Stream
        stream.Type = adTypeBinary
        stream.Open
        stream.LoadFromFile filePath ' ファイルをストリームにロード

        ' 3. ストリームをフィールドに直接流し込む
        ' これにより、VBAのVariant変数を経由することなく、
        ' メモリ効率の高い状態でバイナリデータを転送できる
        rs.Fields("FileData").Value = stream.Read
        
        ' 4. 更新の確定
        rs.Update
        
        stream.Close
        Set stream = Nothing
        Debug.Print "Import Successful: " & filePath
    Else
        Err.Raise vbObjectError + 515, "ImportFileToBlob", "更新対象のレコードが見つかりません。"
    End If

CleanUp:
    On Error Resume Next
    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 "Critical Error in ImportFileToBlob: " & Err.Description, vbCritical
    Resume CleanUp
End Sub

4. 伝説のエンジニアが守るべき「極限の作法」

このコードを実戦に投入するにあたり、以下の3点を魂に刻んでおけ。

I. オブジェクトのライフサイクルと明示的解放

VBAのガベージコレクションは、C#やJavaほど信頼できるものではない。特に `ADODB.Connection` や `Recordset` は、スコープを抜けただけではメモリ上に残り続け、接続プールを枯渇させる原因となる。必ず `Set obj = Nothing` による明示的な解放を、エラーハンドリングの `CleanUp` セクションで行うこと。これが「止まらないシステム」を作る鉄則だ。

II. カーソルロケーションの選択

`conn.CursorLocation = adUseClient` を使用した場合、データはクライアント側のメモリに展開される。大量のBLOBを一度にフェッチ(Fetch)しようとすれば、即座にメモリ不足に陥る。大量データを扱う場合は、`adUseServer` を検討するか、あるいは「IDで1件ずつ取得する」という、一見非効率に見えるが最も安全な戦略を採るべきだ。

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

バイナリデータの書き込みは、通常の文字列操作に比べて極めて重い。`conn.BeginTrans` を呼ぶタイミングには細心の注意を払え。巨大なファイルの書き込み中にトランザクションを保持し続けると、SQL Server側のログファイル(LDF)が肥大化し、データベース全体のパフォーマンスを低下させる。

結言

ADOとStreamオブジェクトを使いこなすことは、単なる「書き方の習得」ではない。それは、コンピュータのリソースが有限であることを理解し、データが流れる「経路」を支配することを意味する。

この知見を正しく実装できたとき、君のシステムは、数年後の過酷な運用環境下でも、静かに、そして確実に動き続けるだろう。

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