エラーとメッセージの処理

Microsoft.Data.SqlClientはサーバーおよびプロバイダーの障害をSqlException を介して報告します。 データベース操作は複数のエラーを返す可能性があるため、結合Errorsだけに頼るのではなく、例外のMessageコレクションを検査しましょう。

データベースエンジンからの警告や情報メッセージは InfoMessageを通じて配信されます。 接続を開く前やメッセージを生成できるコマンドを実行する前に、サブスクライブしてください。

データベース障害は次の順序で対処します:

  1. 操作コンテキストを追加したり、リカバリーアクションを選択できる境界で SqlException をキャッチしましょう。
  2. SqlErrorコレクション内のすべてのErrorsを調べてください。
  3. 認証情報や機密値を記録せずに、構造化された診断フィールドを記録してください。
  4. 接続関連の障害が発生した後の接続状態を確認してください。
  5. エラーが一時的で、操作が安全に再実行できる場合にのみ再試行してください。

この記事の例は、SqlConnection という名前の開いている connection が AdventureWorks sample database を対象としていることを前提としています。 C#の例は using Microsoft.Data.SqlClient;を必要とします。

案内メッセージを受信する

InfoMessageイベントは重大度の低いサーバー メッセージを受け取り、PRINTからの出力も含まれます。 1つのイベントは複数の SqlError オブジェクトを含むことができます。 メッセージを生成できるコマンドを実行する前にサブスクライブしてください:

connection.InfoMessage += (_, eventArgs) =>
{
    foreach (SqlError message in eventArgs.Errors)
    {
        Console.WriteLine(
            $"Info {message.Number}, class {message.Class}: {message.Message}");
    }
};

using var command = new SqlCommand(
    "PRINT N'Connected and ready.';",
    connection);
command.ExecuteNonQuery();

抜粋は以下の通りです:

Info 0, class 0: Connected and ready.

Catch SqlException

自分が扱えるデータベース操作の周りで、SqlException をキャッチしたり、情報を追加したりしましょう。 失敗したデータベース操作を成功に変えるために、広範な例外捕捉を使わないでください。

データベース操作は複数のエラーを返すことがあります。 SqlError内の各Errorsには構造化されたフィールドが含まれています。

プロパティ Meaning
Number データベース エンジンまたはプロバイダーのエラー番号
Class 重要度。
State 同じエラー番号に対して発生箇所を識別できるサーバー定義状態。
LineNumber 報告された行番号。
Procedure 指定された場合の手続き名またはトリガー名
Server エラーについて報告されたサーバー名。
Message 人が判読可能なエラーメッセージ。

SqlException の簡易プロパティ (Number、Class、State、LineNumber、Procedure、Server) は、最初のエラーのみを反映します。 以下の抜粋はメッセージを印刷し、同じバッチ内でエラーを発生させます。 exception.Numberを読み、その後Errorsを繰り返して各エントリーを出力します:

using (var failing = new SqlCommand("""
    PRINT N'Starting controlled operation.';
    RAISERROR (N'The controlled operation failed.', 16, 1);
    """, connection))
{
    try
    {
        await failing.ExecuteNonQueryAsync();
    }
    catch (SqlException exception)
    {
        Console.WriteLine($"First error number: {exception.Number}");

        foreach (SqlError error in exception.Errors)
        {
            Console.WriteLine(
                $"Number={error.Number}; Class={error.Class}; " +
                $"State={error.State}; Message={error.Message}");
        }
    }
}

抜粋は以下の通りです:

First error number: 50000
Number=50000; Class=16; State=1; Message=The controlled operation failed.
Number=0; Class=0; State=1; Message=Starting controlled operation.

PRINT出力はInfoMessageを発生させません。 バッチがSqlExceptionを発生させるエラーを返すと、プロバイダーはそのバッチの情報メッセージをSqlException.Errorsの最後に付け加えます。

この抜粋は制御されたエラーを捉え、そのフィールドを示すだけです。 アプリケーションコードでは、キャッチブロックが実際のリカバリーアクションを選択しない限り、 throw; でリスローします。 文書化されたエラー番号と運用文脈から判断を下してください。 ローカライズされたメッセージのテキストを解析しないでください。

接続状態を確認する

データベースエンジンは各メッセージに重大度クラスを割り当てます。 下位クラスは情報提供用です。 エラーとして扱われるクラスでは、SqlException が生成されます。 重大度の高いエラーは接続をクローズまたは切断することがあります。

エラーが出た後は、接続を再利用する前に State を確認してください:

using var command = new SqlCommand(
    "RAISERROR (N'Controlled failure.', 16, 1);",
    connection);

try
{
    command.ExecuteNonQuery();
}
catch (SqlException)
{
    Console.WriteLine($"Connection state: {connection.State}");
}

例外がキャッチされたからといって接続が使えると決めつけないでください。 重大度16のエラーは通常、オープンのままになりますが、接続を切断する障害によって閉じることがあります。 閉じた接続や壊れた接続を処分し、プールから別の接続を取得してください。 再試行ポリシーを重大度だけで決めないでください。 また、操作は安全に繰り返すことも必要です。

失敗したバッチコマンドを特定する

SqlBatchが失敗した場合、SqlException.BatchCommandは関連するSqlBatchCommandを参照できます。 このプロパティは、プロバイダーが失敗を1つのバッチ コマンドに特定できない場合に null です。 以下の抜粋は2つのバッチコマンドを実行します。 2つ目は失敗します:

using (var batch = new SqlBatch(connection))
{
    batch.BatchCommands.Add(new SqlBatchCommand("SELECT 1;"));
    batch.BatchCommands.Add(new SqlBatchCommand("SELECT 1 / 0;"));

    try
    {
        await batch.ExecuteNonQueryAsync();
    }
    catch (SqlException exception)
    {
        Console.WriteLine($"Error {exception.Number}: {exception.Message}");

        if (exception.BatchCommand is SqlBatchCommand failedCommand)
        {
            Console.WriteLine($"Failed command: {failedCommand.CommandText}");
        }
    }
}

抜粋は以下の通りです:

Error 8134: Divide by zero error encountered.
Failed command: SELECT 1 / 0;

パラメータ化が不十分なアプリケーションでは、コマンド テキストに機密性の高いリテラルが含まれることがあります。 本番ログでは安定したアプリケーション操作名を好み、パラメータ値はデフォルトでログに記録しません。

ユーザーのエラーをInfoMessageにルーティングする

FireInfoMessageEventOnUserErrors では、既定値が false に設定されます。 trueに設定すると、重大度16以下のエラーはInfoMessageを発生させる代わりにSqlExceptionハンドラーに送られます:

connection.InfoMessage += (_, eventArgs) =>
{
    foreach (SqlError message in eventArgs.Errors)
    {
        Console.WriteLine(
            $"Info {message.Number}, class {message.Class}: {message.Message}");
    }
};
connection.FireInfoMessageEventOnUserErrors = true;

using var command = new SqlCommand(
    "RAISERROR (N'Routed to InfoMessage.', 16, 1);",
    connection);
command.ExecuteNonQuery();

例外はスローされません。 抜粋は以下の通りです:

Info 50000, class 16: Routed to InfoMessage.

このオプションは、アプリケーションが意図的にイベントベースのエラーモデルを持っている場合のみ使用してください。呼び出し元コードでは例外が見えなくなるためです。 重大度が17以上のエラーでも SqlExceptionが上がります。 個々のコマンドごとにこのプロパティを切り替えるのではなく、接続設定の一部として一度だけ設定してください。

ログ診断コンテキスト

データを露出せずに失敗を関連付けられるだけの構造化コンテキストを把握すること。 以下の抜粋は、アプリケーション操作名、 ClientConnectionId、および各 SqlErrorのフィールドを記録しています。 ClientConnectionId クライアントの障害とサーバーおよびネットワークの診断を関連付けるのに役立ちます。 それは資格ではありません。

const string operation = "LoadOrders";

using (var load = new SqlCommand("SELECT 1 / 0;", connection))
{
    try
    {
        await load.ExecuteScalarAsync();
    }
    catch (SqlException exception)
    {
        Console.WriteLine(
            $"Operation={operation}; " +
            $"ClientConnectionId={exception.ClientConnectionId}");

        foreach (SqlError error in exception.Errors)
        {
            Console.WriteLine(
                $"Number={error.Number}; Class={error.Class}; " +
                $"State={error.State}; Procedure={error.Procedure}; " +
                $"Line={error.LineNumber}");
        }
    }
}

ClientConnectionId値は接続ごとに異なります。 抜粋版の出力は以下の通りです:

Operation=LoadOrders; ClientConnectionId=<connection ID>
Number=8134; Class=16; State=1; Procedure=; Line=1

また、トランザクションがアクティブだったかどうか、 BatchCommand 利用可能な場合はどのバッチコマンドが失敗したかも記録してください。 組織方針で許可されている場合のみ、サニティ化されたサーバーおよびデータベース識別子を含めてください。 接続文字列、アクセストークン、パスワード、機密パラメータ値はログに記録しないでください。 診断カウンター、イベントソーストレーシング、SqlClientトラブルシューティングガイドを使って、より深い調査を行ってください。