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

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

BLOBデータを制する者は、エンタープライズ開発を制す:ADODB.Streamによる堅牢なバイナリ入出力の実装術

業務システムを構築していると、避けて通れない壁にぶち当たることがある。それは「バイナリデータ(BLOB)のハンドリング」だ。

「画像を表示したい」「PDFをデータベースに保存したい」――。
要件は単純だ。しかし、多くの開発者がここで初歩的なミスを犯し、メモリリークやファイル破損、あるいは「動くが、数MBのファイルが来た瞬間にAccessが落ちる」という、プロダクション環境では致命的な欠陥を抱えたコードを書き上げる。

なぜ、君たちの書くコードは不安定なのか?
それは、「バイナリデータを単なる『バイト配列(Byte Array)』として、メモリ上の変数に詰め込もうとしているからだ」。

本稿では、Access VBAからSQL Serverへ、画像やPDFといったバイナリデータを安全かつ高速に、そして「プロフェッショナルな設計」として扱うための、`ADODB.Stream`を活用した極限の設計手法を伝授する。

1. なぜ「Byte配列への代入」ではいけないのか

初心者は、Recordsetのフィールド値をそのままByte型の変数に代入しようとする。

' --- アンチパターン:これは「動くが、プロの仕事ではない」 ---
Dim myData() As Byte
myData = rs.Fields("FileData").Value ' ← ここでメモリの爆発が起きる可能性がある

この手法がなぜ危険なのか。理由は2つある。

1. メモリの断片化(Fragmentation): 数十MBのファイルを扱う際、VBAのメモリ管理機構が巨大な連続領域を確保できず、`Out of Memory`を吐いてクラッシュする。
2. データ変換の不透明性: バイナリデータをメモリ上で直接操作することは、エンコーディングやストリームの境界線を無視することに等しい。

真のアーキテクトは、データを「変数」としてではなく、「ストリーム(流れ)」として扱う。データの入出力を、メモリ上のバッファを経由させるのではなく、ストリームという「管」を通して直接ファイルとデータベースの間で流すのだ。そのための武器が `ADODB.Stream` である。

2. 実装の核心:ADODB.Streamによる実装パターン

以下に、私が現場で推奨する「プロダクション・クオリティ」のコードを示す。単なるコピペ用ではない。オブジェクトのライフサイクル、エラーハンドリング、そしてリソースの解放までを考慮した設計だ。

2.1 DBからファイルへ:BLOBの抽出(Read)

SQL Serverからバイナリデータを取得し、ローカルのファイルとして保存する関数だ。

''' <summary>
''' SQL ServerのBLOBデータをファイルとして保存する
''' </summary>
''' <param name="conn">有効なADODB.Connectionオブジェクト</param>
''' <param name="sql">対象のレコードを特定するSQL文</param>
''' <param name="targetFilePath">保存先のフルパス</param>
Public Sub ExportBlobToFile(ByRef conn As ADODB.Connection, ByVal sql As String, ByVal targetFilePath As String)
    Dim rs As ADODB.Recordset
    Dim stm As ADODB.Stream
    
    On Error GoTo ErrorHandler

    ' 1. Recordsetのオープン(読み取り専用、前方スクロール)
    Set rs = New ADODB.Recordset
    rs.Open sql, conn, adOpenForwardOnly, adLockReadOnly

    If rs.EOF Then
        Err.Raise vbObjectError + 513, "ExportBlobToFile", "対象のレコードが見つかりません。"
    End If

    ' 2. ADODB.Streamの初期化
    Set stm = New ADODB.Stream
    stm.Type = adTypeBinary ' バイナリモードを指定
    stm.Open

    ' 3. フィールドからストリームへ書き込み
    ' rs.Fields(0).Value に直接代入するのではなく、ストリームを経由させる
    stm.Write rs.Fields(0).Value

    ' 4. ファイルへの書き出し
    ' stm.Positionを0に戻してから保存するのが定石
    stm.Position = 0
    stm.SaveToFile targetFilePath, adSaveCreateOverWrite

    Debug.Print "Success: " & targetFilePath

CleanUp:
    ' 5. リソースの確実な解放(これが重要)
    On Error Resume Next
    If Not stm Is Nothing Then
        stm.Close
        Set stm = Nothing
    End If
    If Not rs Is Nothing Then
        If rs.State = adStateOpen Then rs.Close
        Set rs = Nothing
    End If
    Exit Sub

ErrorHandler:
    MsgBox "Error in ExportBlobToFile: " & Err.Description, vbCritical
    Resume CleanUp
End Sub

2.2 ファイルからDBへ:BLOBの注入(Write)

逆に、ローカルのファイルを読み込んでSQL Serverのフィールドへ書き込むパターンだ。

''' <summary>
''' ファイルのバイナリデータをSQL ServerのBLOBフィールドに更新する
''' </summary>
''' <param name="conn">有効なADODB.Connectionオブジェクト</param>
''' <param name="updateSql">UPDATE文</param>
''' <param name="sourceFilePath">読み込むファイルのフルパス</param>
Public Sub ImportFileToBlob(ByRef conn As ADODB.Connection, ByVal updateSql As String, ByVal sourceFilePath As String)
    Dim rs As ADODB.Recordset
    Dim stm As ADODB.Stream

    On Error GoTo ErrorHandler

    ' 1. ファイルの存在チェック(基本中の基本)
    If Dir(sourceFilePath) = "" Then
        Err.Raise vbObjectError + 514, "ImportFileToBlob", "指定されたファイルが存在しません。"
    End If

    ' 2. ストリームの準備(ファイル読み込み用)
    Set stm = New ADODB.Stream
    stm.Type = adTypeBinary
    stm.Open
    stm.LoadFromFile sourceFilePath

    ' 3. Recordsetのオープン(更新用にOptimisticロックを使用)
    Set rs = New ADODB.Recordset
    ' ※UPDATE文ではなく、SELECT文で対象レコードを掴むのがADOの定石
    ' ここでは例として、対象レコードを特定するSELECT文を渡す前提とする
    rs.Open updateSql, conn, adOpenStatic, adLockOptimistic

    If Not rs.EOF Then
        ' 4. ストリームの内容をフィールドに流し込む
        rs.Fields(0).AppendChunk stm.Read
        rs.Update
        Debug.Print "Success: File uploaded to DB."
    Else
        Err.Raise vbObjectError + 515, "ImportFileToBlob", "更新対象のレコードが見つかりません。"
    End If

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

ErrorHandler:
    MsgBox "Error in ImportFileToBlob: " & Err.Description, vbCritical
    Resume CleanUp
End Sub

3. アーキテクトの視点:設計における3つの鉄則

コードが書けただけでは、まだ未熟だ。システムとして「堅牢」であるためには、以下の設計思想を実装に組み込まなければならない。

① トランザクションの整合性(The Ghost File Problem)

バイナリデータの扱いで最も恐ろしいのは、「DBの更新は失敗したが、ファイルだけがローカルに生成された(あるいはその逆)」という不整合だ。

  • 設計指針:
  • ファイル操作を伴う場合、必ず `conn.BeginTrans` と `conn.CommitTrans` を適切に使い分けること。
  • DB更新に失敗した場合は、作成したファイルを `Kill` 文で削除するリカバリ処理を `ErrorHandler` に組み込む。

② `AppendChunk` の活用

巨大なファイル(例えば100MBを超える動画など)を扱う場合、`rs.Fields(0).Value = stm.Read` と書くと、結局メモリ上に全データを展開してしまう。
`ADODB.Recordset.Fields(idx).AppendChunk` メソッドを使用することで、ストリームから分割してデータを流し込むことが可能になり、メモリ消費を劇的に抑えられる。

③ オブジェクトのライフサイクル管理

VBAにおいて、`Set obj = Nothing` は単なるマナーではない。特に `ADODB.Stream` や `Recordset` は、OSレベルのファイルハンドルやネットワークソケットを掴んでいる。
エラーが発生した際に `CleanUp` ラベルへ飛ばし、「成功しても失敗しても、必ず `Close` して `Nothing` を代入する」。この徹底が、数ヶ月稼働させた後に発生する「原因不明のAccessフリーズ」を防ぐ唯一の手段だ。

結論

Access VBAを用いた開発において、SQL Serverをバックエンドに据える判断は、スケーラビリティの観点から極めて正しい。しかし、その接続路(ADO)におけるデータの扱いが稚拙であれば、システム全体の信頼性は崩壊する。

`ADODB.Stream` を使いこなし、データを「メモリ上の塊」ではなく「流れるストリーム」として捉えること。このパラダイムシフトこそが、君を単なるプログラマーから、信頼されるシステムアーキテクトへと進化させるのだ。

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