【テクニカル・上級編】ADOの接続エラーを詳細にキャッチ:Errorsコレクションによる原因特定とログ出力 – Access VBA解析バイブル

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

虚像のErrオブジェクトを捨て、真実のErrorsコレクションを掴め:ADOにおける深層エラー解析の極意

Access VBAを用いた大規模システム、あるいはSQL Serverをバックエンドに据えたエンタープライズ・アーキテクチャにおいて、エンジニアが直面する最も忌まわしく、かつ最も重要な瞬間がある。それは「接続エラー」または「実行時エラー」が発生した瞬間だ。

多くの開発者は、`MsgBox Err.Description` と出力して満足する。しかし、それはプロフェッショナルではない。それは、事故現場に駆けつけた警官が「何かが起きました」と報告するだけで、原因がガソリン漏れなのか、エンジン爆発なのか、あるいは外部からの衝撃なのかを一切調査しないのと同じだ。

本稿では、ADO(ActiveX Data Objects)が隠蔽している「真実のエラー」を、`Errors`コレクションから引き出し、システムの健全性を担保するためのログ出力と、オブジェクトのライフサイクル管理について、その深淵を解説する。

1. 階層化されたエラーの構造:なぜ `Err.Number` では足りないのか

AccessにおけるDAO(Data Access Objects)とADOの決定的な違いは、その設計思想と「エラーの所在」にある。

  • DAO: ローカルなファイル(ACE/Jetエンジン)を操作するための設計。エラーは比較的シンプルであり、ファイル破損や制約違反など、ローカルなコンテキストに帰結する。
  • ADO: リモートサーバー(SQL Server等)との通信を前提とした、多層的なプロトコル。

ADOにおいてエラーが発生した際、VBAの標準的な `Err` オブジェクトが拾うのは、あくまでADOプロバイダがVBAに投げた「最終的な要約」に過ぎない。

例えば、SQL Server側で「主キー制約違反(Error 2627)」が発生したとする。ADOを経由してVBAにエラーが伝播するとき、VBAの `Err.Number` にはADO側のエラー番号(例: `-2147217871`)が格納される。これでは、DB側で何が起きたのか、エンジニアは一切の手がかりを失うことになる。

真実は、`ADODB.Connection` オブジェクトが保持している `Errors` コレクションの中に、SQL Serverから直接送られてきたNativeError(ネイティブエラー番号)として眠っているのだ。

2. 実装:Errorsコレクションを解剖するロギング・エンジン

シニアエンジニアとして、我々が実装すべきは、単なるエラー通知ではない。エラーの「文脈(Context)」を完全に保存するロギング・メカニズムである。

以下に、ADOの接続および実行時に発生した詳細なエラーを、SQL Serverのネイティブコードを含めて抽出する堅牢な実装例を示す。

Option Explicit

' ======================================================================================
' Module: modADO_AdvancedErrorHandling
' Description: ADOのエラーを詳細に解析し、ログ出力を行うアーキテクチャ・モジュール
' ======================================================================================

Public Sub ExecuteSecureCommand(ByVal connectionString As String, ByVal sql As String)
    Dim conn As ADODB.Connection
    Dim cmd As ADODB.Command
    
    ' オブジェクトの初期化
    Set conn = New ADODB.Connection
    Set cmd = New ADODB.Command

    On Error GoTo ErrorHandler

    ' 1. 接続の確立
    ' パフォーマンス最適化のため、必要に応じてConnectionTimeoutを明示的に設定
    conn.ConnectionTimeout = 15
    conn.Open connectionString

    ' 2. コマンドの設定
    With cmd
        .ActiveConnection = conn
        .CommandText = sql
        .CommandType = adCmdText
        .Execute
    End With

    Debug.Print "Execution Successful."

CleanExit:
    ' 3. オブジェクトの明示的解放 (メモリリーク防止の鉄則)
    ' Access VBAにおいて、COMオブジェクトの解放を怠ることは、
    ' 長時間稼働するシステムにおけるメモリ断片化と、予期せぬ「オブジェクトが使用中」エラーの温床となる。
    On Error Resume Next
    If Not cmd Is Nothing Then Set cmd = Nothing
    If Not conn Is Nothing Then
        If conn.State = adStateOpen Then conn.Close
        Set conn = Nothing
    End If
    Exit Sub

ErrorHandler:
    ' 4. ADO特有のErrorsコレクション解析
    ' 標準のErrオブジェクトではなく、Connectionオブジェクトが持つErrorsを解析する
    Call LogDetailedADODetails(conn, Err)
    
    ' ユーザーへの通知(本番環境では抽象化されたメッセージを推奨)
    MsgBox "システムエラーが発生しました。管理者に連絡してください。", vbCritical, "Critical Error"
    
    Resume CleanExit
End Sub

''' <summary>
''' ADOのErrorsコレクションを走査し、詳細なエラー情報をログに出力する
''' </summary>
Private Sub LogDetailedADODetails(ByRef conn As ADODB.Connection, ByRef vbaErr As Err)
    Dim errObj As ADODB.Error
    Dim logPath As String
    Dim logMsg As String
    
    logPath = CurrentProject.Path & "\system_error.log"
    logMsg = "--- Error Log Entry: " & Now & " ---" & vbCrLf
    
    ' VBA標準のエラー情報を付加
    logMsg = logMsg & "[VBA Error] No: " & vbaErr.Number & " | Desc: " & vbaErr.Description & vbCrLf
    
    ' ADOのErrorsコレクションを走査
    ' ここが肝:ConnectionがOpenしている場合、その中にServer側の詳細が格納されている
    If Not conn Is Nothing Then
        If conn.Errors.Count > 0 Then
            logMsg = logMsg & "[ADO Deep Dive] Found " & conn.Errors.Count & " error(s) in collection:" & vbCrLf
            
            For Each errObj In conn.Errors
                logMsg = logMsg & "  -> [NativeError] " & errObj.NativeError & vbCrLf ' SQL Server側のエラー番号
                logMsg = logMsg & "     [SQLState] " & errObj.SQLState & vbCrLf    ' 標準SQL状態コード
                logMsg = logMsg & "     [Description] " & errObj.Description & vbCrLf
                logMsg = logMsg & "     [Source] " & errObj.Source & vbCrLf
            Next errObj
        Else
            logMsg = logMsg & "[ADO Info] No specific ADO errors found in collection." & vbCrLf
        End If
    Else
        logMsg = logMsg & "[ADO Info] Connection object is Nothing. Cannot retrieve deep errors." & vbCrLf
    End If
    
    logMsg = logMsg & "-------------------------------------------" & vbCrLf
    
    ' ログファイルへの書き出し (FileSystemObjectを使用せず、低レイヤーの書き出しを想定)
    ' 実務では、ここをデータベースのログテーブルへのINSERTに置き換えるのが定石。
    Call WriteLogToFile(logPath, logMsg)
End Sub

Private Sub WriteLogToFile(ByVal filePath As String, ByVal content As String)
    Dim fso As Object
    Dim ts As Object
    
    Set fso = CreateObject("Scripting.FileSystemObject")
    ' 8 = ForAppending, True = Create if not exists
    Set ts = fso.OpenTextFile(filePath, 8, True)
    ts.WriteLine content
    ts.Close
    
    Set ts = Nothing
    Set fso = Nothing
End Sub

3. アーキテクチャの極限:メモリ管理とオブジェクトのライフサイクル

コード例の中で、私は `Set obj = Nothing` を極めて厳格に、かつ `On Error Resume Next` を伴って記述している。これは単なる「行儀の良さ」ではない。

メモリの断片化(Fragmentation)への対抗

Access VBAは、シングルスレッドのCOM環境で動作する。ADOのような外部プロセス(SQL ServerやOLE DBプロバイダ)と通信するオブジェクトは、メモリ上に「参照カウント」を保持する。
もし、エラー発生時に `ErrorHandler` へジャンプし、オブジェクトの解放処理を飛ばしてしまった場合、そのオブジェクトはメモリ上に「幽霊(Zombie Object)」として残り続ける。これが繰り返されると、Accessのプロセス自体が肥大化し、最終的には `Out of Memory` や、不可解なクラッシュを引き起こす。

接続の「状態」とエラーの「タイミング」

重要な知見を一つ。`Errors` コレクションに詳細が格納されるのは、「接続が確立されている最中、または実行中に通信が発生した時」に限られる。
接続そのものが失敗した場合(例:ネットワーク断、サーバーダウン)、`conn` オブジェクト自体が正しく生成されていない、あるいは `Open` メソッド内で即座に制御が戻るため、`conn.Errors` にアクセスすること自体がエラーを誘発する可能性がある。

そのため、私の実装では `If Not conn Is Nothing Then` というガード句を入れ、オブジェクトの生存を確認してから解析を行っている。これが、堅牢なシステムを構築する上での「防衛的プログラミング」の真髄である。

結論:エンジニアの責務

エラーハンドリングとは、単にプログラムを落とさないための技術ではない。「システムがなぜ死んだのかを、後から検証可能な形で残す」という、運用のための設計である。

`Err.Number` という表層的な情報に甘んじるか、`Errors` コレクションという深淵に潜り込み、SQL Serverの悲鳴(NativeError)を正確に記録するか。その差が、シニアエンジニアと、単なるコーダーを分かつ境界線である。

アーキテクチャの整合性を保つのは、常にコードの隅々にまで行き届いた、こうした細部への執着なのだ。

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