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

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

境界を越えるストリーム:ADOによるSQL Server BLOBデータの極限制御

Access VBAの世界に浸っているエンジニアの多くは、DAO(Data Access Objects)の作法に縛られている。Jet/ACEエンジンを前提としたDAOは、ローカルなファイルベースのDB操作には極めて優秀だ。しかし、我々が向き合うべき現実、すなわち「SQL Serverという巨大なインフラとの共生」においては、その境界線はあまりにも脆弱である。

特に、画像、PDF、あるいは設計図といったBLOB(Binary Large Object)データを扱う際、安易に`Variant`型に全データを流し込むような実装は、メモリ空間の断片化(Fragmentation)を招き、やがてAccessのプロセスを「Out of Memory」の奈落へと突き落とす。

本稿では、ADO(ActiveX Data Objects)を用いた、メモリ効率を極限まで高めたバイナリデータのストリーミング技術——`GetChunk`および`AppendChunk`の真髄を解説する。これは単なるコーディングテクニックではない。リソースのライフサイクルを掌握するための、アーキテクトとしての生存戦略である。

1. なぜ `Variant` への一括代入は「悪」なのか

初学者はこう書く。
`vData = rs.Fields(“FileData”).Value`

これは、バイナリデータが数百KB程度であれば問題ない。しかし、数MB、数十MBのファイルが並行してリクエストされる環境では、この一行が致命傷となる。

1. メモリの爆発的消費: `Variant`型は、データのサイズに応じてメモリを動的に確保する。巨大なバイナリを一度に確保しようとすると、連続したメモリ領域の確保に失敗する。
2. ガベージコレクションの不在: VBAには高度なGC(Garbage Collection)が存在しない。巨大なオブジェクトがスコープを抜けても、参照カウントが正しく管理されない限り、メモリは解放されない。
3. ネットワーク・レイテンシ: 一括取得は、ネットワーク・バッファを一度に占有し、他のクエリの実行を阻害する。

我々が取るべき道は、データを「塊(Chunk)」として扱い、細切れに流し込むストリーミング処理である。

2. GetChunk:バイナリデータの断片化読み出し

SQL Serverからファイルへとデータを書き出す際、`GetChunk`メソッドは、メモリへの負荷を一定に保つための「ダムの放流」のような役割を果たす。

実装例:SQL Serverからローカルファイルへの抽出

''' <summary>
''' SQL ServerのBLOBデータをファイルへストリーミング出力する
''' </summary>
''' <param name="rs">開いているADODB.Recordset</param>
''' <param name="fieldName">BLOBフィールド名</param>
''' <param name="destPath">出力先パス</param>
Public Sub ExportBlobToFile(ByRef rs As ADODB.Recordset, ByVal fieldName As String, ByVal destPath As String)
    On Error GoTo Err_Handler

    Dim fnum As Integer
    Dim byteChunk As Variant
    Dim chunkSize As Long
    Dim fieldObj As ADODB.Field
    
    ' チャンクサイズの設定(64KB程度がメモリとI/Oのバランスが良い)
    chunkSize = 65536 
    
    Set fieldObj = rs.Fields(fieldName)
    fnum = FreeFile
    
    ' バイナリ書き込みモードでファイルを開く
    Open destPath For Binary Access Write As #fnum
    
    ' GetChunkを使用して、メモリ消費を抑えつつループで読み出す
    ' 第1引数にチャンクサイズを指定することで、その分量だけを切り出す
    Do While Not IsNull(fieldObj.GetChunk(chunkSize))
        byteChunk = fieldObj.GetChunk(chunkSize)
        
        ' 取得したチャンクをファイルに書き込む
        Put #fnum, , byteChunk
    Loop
    
    Close #fnum
    Debug.Print "Export Completed: " & destPath

Exit_Handler:
    ' オブジェクトの明示的な解放は、アーキテクトの義務である
    Set fieldObj = Nothing
    Exit Sub

Err_Handler:
    If fnum > 0 Then Close #fnum
    MsgBox "Export Error: " & Err.Description, vbCritical
    Resume Exit_Handler
End Sub

3. AppendChunk:バイナリデータの断片化書き込み

逆に、ローカルファイルをSQL Serverへ格納する場合、`AppendChunk`を用いる。ここでの肝は、「フィールドへの値代入を一度も行わない」ことだ。

通常、`rs.Fields(“Data”).Value = byteData` と記述すると、その瞬間にメモリ上に巨大なオブジェクトが生成される。`AppendChunk`を使用すれば、ファイルから読み込んだ小さな断片を、そのままデータベースのストリームへと流し込むことができる。

実装例:ローカルファイルからSQL Serverへの格納

''' <summary>
''' ファイルからSQL ServerのBLOBフィールドへストリーミング書き込みを行う
''' </summary>
''' <param name="rs">更新対象のADODB.Recordset(adLockOptimistic必須)</param>
''' <param name="fieldName">BLOBフィールド名</param>
''' <param name="srcPath">ソースファイルパス</param>
Public Sub ImportFileToBlob(ByRef rs As ADODB.Recordset, ByVal fieldName As String, ByVal srcPath As String)
    On Error GoTo Err_Handler

    Dim fnum As Integer
    Dim byteChunk As Variant
    Dim chunkSize As Long
    Dim fieldObj As ADODB.Field

    chunkSize = 65536
    Set fieldObj = rs.Fields(fieldName)
    
    ' SQL Server側のフィールドを初期化(重要:既存データをクリアする)
    ' AppendChunkは既存データの後ろに追加していく性質があるため、
    ' 新規書き込み時は一旦Nullにするか、空のバイナリで上書きする必要がある
    fieldObj.Value = Null 

    fnum = FreeFile
    Open srcPath For Binary Access Read As #fnum

    ' ファイルの終端に達するまで、チャンクごとに読み込みと書き込みを繰り返す
    Do While Not EOF(fnum)
        ' ファイルからチャンク読み込み
        byteChunk = ReadFileChunk(fnum, chunkSize)
        
        ' 取得したチャンクをDBへAppend
        If Not IsEmpty(byteChunk) Then
            fieldObj.AppendChunk byteChunk
        End If
    Loop

    Close #fnum
    
    ' トランザクションの確定(呼び出し元で制御するのが理想)
    rs.Update
    Debug.Print "Import Completed: " << srcPath

Exit_Handler:
    Set fieldObj = Nothing
    Exit Sub

Err_Handler:
    If fnum > 0 Then Close #fnum
    MsgBox "Import Error: " & Err.Description, vbCritical
    Resume Exit_Handler
End Sub

''' <summary>
''' ファイルから指定サイズ分を読み出すヘルパー関数
''' </summary>
Private Function ReadFileChunk(ByRef fnum As Integer, ByVal size As Long) As Variant
    Dim buffer() As Byte
    Dim bytesRead As Long
    
    ReDim buffer(0 To size - 1)
    Get #fnum, , buffer
    
    ' 実際に読み込めたバイト数を取得(EOF付近の処理)
    ' ※実際の実装では、Getの戻り値やファイル長を厳密に管理すべき
    ' ここでは簡略化のため、読み込んだデータをそのまま返す
    ReadFileChunk = buffer
End Function

4. アーキテクチャ設計における極限の注意点

これらを利用する際、単にコードを書くだけでは不十分だ。システムの堅牢性を担保するために、以下の3点を肝に銘じておけ。

① CursorLocation の選択

BLOBのストリーミングを行う場合、`rs.CursorLocation = adUseServer` を検討せよ。`adUseClient`(クライアントサイドカーソル)を使用すると、ADOは結果セット全体をクライアント側のメモリにキャッシュしようとする。これは、巨大なバイナリを扱う際の「メモリ爆発」の主犯となる。

② トランザクションの整合性

`AppendChunk`は、複数の呼び出しを経て一つのレコードを完成させる。もし途中でネットワークエラーやアプリケーションの強制終了が発生した場合、DBには「不完全なバイナリ」が残るリスクがある。
必ず `Connection.BeginTrans` と `Connection.CommitTrans` を用い、書き込みプロセス全体をアトミック(不可分)にせよ。

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

VBAにおいて、`Set rs = Nothing` は単なる作法ではない。特にADOのRecordsetやConnectionオブジェクトは、COM(Component Object Model)の参照カウントに依存している。
ストリーミング処理のような高負荷なループ内では、各ステップで参照が適切に解放されているか、あるいはスコープを限定しているかを常に意識せよ。

結言

Access VBAを「単なるマクロツール」と侮る者は、大規模なシステム連携の壁に必ずぶち当たる。しかし、ADOの低レイヤなメソッドを理解し、メモリとI/Oの挙動を制御下に置くことができれば、AccessはSQL Serverという強靭なバックエンドを操る、極めて強力なクライアント・エンジンへと昇華する。

技術の深淵を知れ。細部に宿るリソース管理こそが、システムの命運を分けるのだ。

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