虚像の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)を正確に記録するか。その差が、シニアエンジニアと、単なるコーダーを分かつ境界線である。
アーキテクチャの整合性を保つのは、常にコードの隅々にまで行き届いた、こうした細部への執着なのだ。
