「Err.Number」の限界を超えて:ADO Errorsコレクションによる、SQL Server接続における極限のデバッグ・アーキテクチャ
現場のエンジニアが書くコードの多くは、致命的な欠陥を抱えている。
それは、エラーハンドリングの「浅さ」だ。
`On Error GoTo Err_Handler` と書き、`MsgBox Err.Number & ” : ” & Err.Description` を出力して満足している。もし君がそう考えているのなら、君はまだ「システム」を運用しているのではなく、単に「ツール」を作っているに過ぎない。
特に、Access VBAをフロントエンドとし、バックエンドにSQL Serverを据えた分散アーキテクチャにおいて、クライアント側の `Err` オブジェクトが提供する情報は、氷山の一角に過ぎない。サーバー側で何が起きたのか、制約違反の詳細なコンテキストは何なのか、トランザクションがどの段階で破綻したのか。それらを知るためには、ADO(ActiveX Data Objects)の深淵、すなわち `Errors` コレクションを掌握しなければならない。
本稿では、レガシーなAccess環境からモダンなSQL Serverへの橋渡しにおいて、真に信頼に足る「詳細エラーログ生成メカニズム」の設計思想を解説する。
1. アーキテクチャの断絶:DAOとADOの決定的な違い
まず、根本的な認識を改める必要がある。DAO(Data Access Objects)とADOは、エラーの捉え方そのものが異なる。
- DAO は、Jet/ACEエンジンという「ファイル操作」の延長線上にある。エラーはローカルなファイルシステムやファイル構造に起因することが多く、エラー情報は比較的単純だ。
- ADO は、ネットワーク越しに「プロトコル(TDS等)」を介してサーバーと対話する。ここでのエラーは、ネットワークの瞬断、サーバー側のデッドロック、制約違反、タイムアウトなど、多層的なレイヤーから発生する。
クライアント側の `Err` オブジェクトは、ADOが受け取った「最初の、あるいは要約されたエラー」しか保持しない。しかし、SQL Serverは一つの操作に対して複数のエラーを返すことがある。この「エラーの連鎖」を捕捉できるのは、`ADODB.Connection` オブジェクトが保持する `Errors` コレクションだけだ。
2. 武器の選定:Errorsコレクションの深掘り
SQL Serverとの通信において、真に価値のある情報は以下のプロパティに集約されている。
- `Description`: サーバーが返した詳細なメッセージ(「主キー制約違反」などの具体的な内容)。
- `NativeError`: SQL Server固有のエラー番号。これこそが、MSDNを引くための真の鍵だ。
- `SQLState`: ANSI標準のSQLステート。
- `Severity`: エラーの深刻度。
これらを無視して `Err.Description` だけをログに書き込むのは、現場のエンジニアとしては怠慢と言わざるを得ない。
3. 実装:極限のデバッグ・ログ生成モジュール
以下に、私が長年の現場で磨き上げてきた、メモリ管理と詳細捕捉を両立させたログ生成の設計パターンを示す。ここでは、Windows APIを用いて実行時のプロセスIDやシステム情報を付与し、ログのトレーサビリティを極限まで高めている。
Option Compare Database
Option Explicit
' --- Windows API Declarations ---
' システムの文脈をログに刻むために、プロセスIDを取得する
Private Declare PtrSafe Function GetCurrentProcessId Lib "kernel32" () As Long
''' <summary>
''' 高精度DBエラーロガー
''' ADOのErrorsコレクションを走査し、詳細なコンテキストをログ出力する。
''' </summary>
Public Module ErrorLogger
Private Const LOG_FILE_PATH As String = "C:\Logs\DB_Error_Log.txt" ' 本番環境では適切なパスへ
''' <summary>
''' ADO Connectionオブジェクトからエラーを抽出し、詳細ログを生成する
''' </summary>
''' <param name="cn">エラーが発生したADODB.Connectionオブジェクト</param>
''' <param name="sql">実行しようとして失敗したSQL文(任意)</param>
Public Sub LogADODetails(ByRef cn As ADODB.Connection, Optional ByVal sql As String = "")
Dim errObj As ADODB.Error
Dim logMsg As String
Dim pid As Long
pid = GetCurrentProcessId()
' 1. 基本情報の構築
logMsg = "==================================================" & vbCrLf
logMsg = logMsg & "TIMESTAMP: " & Now & vbCrLf
logMsg = logMsg & "PID: " & pid & " | USER: " & Environ("USERNAME") & vbCrLf
logMsg = logMsg & "--------------------------------------------------" & vbCrLf
If sql <> "" Then
logMsg = logMsg & "FAILED SQL: " & sql & vbCrLf
End If
' 2. ADO Errorsコレクションの走査 (ここが核心)
' 単一のエラーではなく、サーバーから返されたエラーの列をすべて記録する
If cn.Errors.Count > 0 Then
logMsg = logMsg & "[ADO Errors Found: " & cn.Errors.Count & "]" & vbCrLf
For Each errObj In cn.Errors
logMsg = logMsg & " >> NativeError: " & errObj.NativeError & vbCrLf
logMsg = logMsg & " SQLState: " & errObj.SQLState & vbCrLf
logMsg = logMsg & " Severity: " & errObj.Severity & vbCrLf
logMsg = logMsg & " Description: " & errObj.Description & vbCrLf
Next errObj
Else
' Connection.Errorsが空の場合、VBA標準のErrオブジェクトを確認する
logMsg = logMsg & "[Standard VBA Error]" & vbCrLf
logMsg = logMsg & " >> Err.Number: " & Err.Number & vbCrLf
logMsg = logMsg & " Err.Description: " & Err.Description & vbCrLf
End If
logMsg = logMsg & "==================================================" & vbCrLf & vbCrLf
' 3. ファイルへの書き出し (Appendモード)
Call WriteToLogFile(logMsg)
End Sub
Private Sub WriteToLogFile(ByVal text As String)
On Error Resume Next ' ログ出力自体の失敗でメイン処理を止めない
Dim fso As Object
Set fso = CreateObject("Scripting.FileSystemObject")
' ファイルがなければ作成、あれば追記
Dim ts As Object
Set ts = fso.OpenTextFile(LOG_FILE_PATH, 8, True)
ts.WriteLine text
ts.Close
' オブジェクトの明示的解放(メモリ最適化の鉄則)
Set ts = Nothing
Set fso = Nothing
On Error GoTo 0
End Sub
End Module
実践的な利用パターン(トランザクション管理下)
エラーログを機能させるには、呼び出し側のアーキテクチャが重要だ。特にトランザクション処理においては、エラー発生時に「即座にロールバック」しつつ、「エラーの詳細を捕捉」するという順序を厳守しなければならない。
Public Sub ExecuteComplexTransaction()
Dim cn As ADODB.Connection
Dim sql As String
Set cn = New ADODB.Connection
sql = "UPDATE Inventory SET Stock = Stock - 1 WHERE ItemID = 101" ' 例としてのSQL
On Error GoTo Transaction_Error
' 接続設定
cn.ConnectionString = "Provider=SQLNCLI11;Data Source=YourServer;Initial Catalog=YourDB;Integrated Security=SSPI;"
cn.Open
' トランザクション開始
cn.BeginTrans
' 業務ロジック実行
cn.Execute sql
' 全て成功すればコミット
cn.CommitTrans
Debug.Print "Transaction Committed Successfully."
Exit_Sub:
' オブジェクトの解放は、成功・失敗に関わらず必ず行う
If Not cn Is Nothing Then
If cn.State = adStateOpen Then cn.Close
Set cn = Nothing
End If
Exit Sub
Transaction_Error:
' 1. まずロールバック(接続が生きている場合のみ)
If Not cn Is Nothing Then
If cn.State = adStateOpen Then
cn.RollbackTrans
Debug.Print "Transaction Rolled Back."
End If
' 2. エラーログの詳細出力を実行
' ここで、実行しようとしたSQLを渡すことで、再現性を確保する
Call LogADODetails(cn, sql)
End If
MsgBox "致命的なエラーが発生しました。詳細はログを確認してください。", vbCritical
Resume Exit_Sub
End Module
4. 極限の知見:メモリ管理とオブジェクトのライフサイクル
シニアエンジニアとして、以下の2点には常に神経を尖らせてほしい。
オブジェクトの明示的解放 (`Set obj = Nothing`)
Access VBAはCOM(Component Object Model)の塊だ。特にADOオブジェクトは外部プロセス(SQL Server)と密接に結合している。エラーハンドリング内で `Set cn = Nothing` を怠れば、たとえ `RollbackTrans` を呼んでいても、サーバー側で「ゾンビ・トランザクション」が残り続け、他のユーザーのプロセスをロックし続ける原因となる。
Windows APIの活用
ログに `GetCurrentProcessId` を含める理由は、マルチユーザー・マルチプロセス環境において、どのプロセス(あるいはどのインスタンスのAccess)がエラーを吐いたのかを特定するためだ。これは、不具合の再現性を追い込む際の「決定的な証拠」となる。
結論
エラーハンドリングとは、単に「止まらないようにすること」ではない。「何が起きたのかを、後から誰が見ても完璧に再現・解析できるようにすること」である。
`Err.Number` という甘えを捨て、`ADODB.Errors` という真実を掴め。それが、レガシーな資産を、堅牢なエンタープライズ・システムへと昇華させるための唯一の道だ。
