接続の回復

接続の回復では、失敗したデータベース コマンドを自動的に再試行します。 この機能は、"実行戦略" を指定することによって任意のデータベースで使用できます。これには、エラーを検出してコマンドを再試行するために必要なロジックがカプセル化されています。 EF Core プロバイダーは、特定のデータベース エラー条件と最適な再試行ポリシーに合わせた実行戦略を提供できます。

例として、SQL Server プロバイダーには、SQL Server (SQL Azure を含む) に特化した実行戦略が用意されています。 これは再試行できる例外の種類を認識し、最大再試行回数や再試行の間隔などについての適切な既定値があります。

実行戦略は、コンテキストのオプションを構成するときに指定します。 これは通常、派生コンテキストの OnConfiguring メソッドで行います。

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    optionsBuilder
        .UseSqlServer(
            @"Server=(localdb)\mssqllocaldb;Database=EFMiscellanous.ConnectionResiliency;Trusted_Connection=True;ConnectRetryCount=0",
            options => options.EnableRetryOnFailure());
}

あるいは、ASP.NET Core アプリケーションの場合は Startup.cs で行います。

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<PicnicContext>(
        options => options.UseSqlServer(
            "<connection string>",
            providerOptions => providerOptions.EnableRetryOnFailure()));
}

Note

失敗時の再試行を有効にすると、EF が内部的に結果セットをバッファーに格納するため、大きな結果セットを返すクエリのメモリ要件が大幅に増加する可能性があります。 詳細については、バッファリングとストリーミングを参照してください。

カスタム実行戦略

既定値を変更する必要がある場合は、独自のカスタム実行戦略を登録するメカニズムがあります。

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    optionsBuilder
        .UseMyProvider(
            "<connection string>",
            options => options.ExecutionStrategy(...));
}

実行戦略とトランザクション

エラーに対して自動的に再試行を行う実行戦略は、失敗した再試行ブロックで各操作を再生できる必要があります。 再試行を有効にすると、EF Core を使用して実行する各操作は、その独自の再試行可能な操作になります。 つまり、一時的なエラーが発生した場合、SaveChanges() への各クエリと各呼び出しは 1 つのユニットとして再試行されます。

一方、BeginTransaction() を使用してトランザクションを開始するコードの場合、1 ユニットとして扱う必要のある独自の操作グループを定義しています。エラーが発生した場合、トランザクション内のすべてがプレイバックされます。 実行戦略を使用しているときに、これを行うことを試行した場合、次のような例外が発生します。

InvalidOperationException: 構成された実行戦略 'SqlServerRetryingExecutionStrategy' は、ユーザーが開始したトランザクションをサポートしていません。 'DbContext.Database.CreateExecutionStrategy()' から返された実行戦略を使用して、再試行可能なユニットとしてトランザクション内のすべての操作を実行します。

この解決策では、実行する必要があるすべてを表すデリゲートを使用して実行戦略を手動で呼び出します。 一時的なエラーが発生した場合、実行戦略によってデリゲートが再び呼び出されます。


using var db = new BloggingContext();
var strategy = db.Database.CreateExecutionStrategy();

strategy.Execute(
    () =>
    {
        using var context = new BloggingContext();
        using var transaction = context.Database.BeginTransaction();

        context.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/dotnet" });
        context.SaveChanges();

        context.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/visualstudio" });
        context.SaveChanges();

        transaction.Commit();
    });

この方法は、アンビエント トランザクションで使用することもできます。


using var context1 = new BloggingContext();
context1.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/visualstudio" });

var strategy = context1.Database.CreateExecutionStrategy();

strategy.Execute(
    () =>
    {
        using var context2 = new BloggingContext();
        using var transaction = new TransactionScope();

        context2.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/dotnet" });
        context2.SaveChanges();

        context1.SaveChanges();

        transaction.Complete();
    });

トランザクションのコミット エラーとべき等の問題

一般に、接続エラーが発生した場合は、現在のトランザクションがロールバックされます。 ただし、トランザクションのコミット中に接続が切断された場合、トランザクションの結果の状態は不明になります。

既定では、実行戦略によって、トランザクションがロールバックされた場合と同様に操作が再試行されます。ただし、そうならない場合、新しいデータベースの状態が矛盾する場合は例外が発生し、あるいは自動生成されたキー値を使用して新しい行を挿入する場合など、操作が特定の状態に依存しない場合はデータ破損を招くことがあります。

これに対処するには、いくつかの方法があります。

オプション 1- (ほぼ) 何もしない

トランザクションのコミット中に接続エラーになる可能性は低いので、実際にこの状況が発生しても、アプリケーションでエラーが発生するだけなら許容できることがあります。

ただし、重複する行を追加するのではなく、例外がスローされるようにするために、ストア生成キーを使用しないようにする必要があります。 クライアントによって生成された GUID 値またはクライアント側の値ジェネレーターを使用することを検討してください。

オプション 2 - アプリケーションの状態を再構築する

  1. 現在の DbContext を破棄します。
  2. 新しい DbContext を作成し、アプリケーションの状態をデータベースから復元します。
  3. 最後の操作が正常に完了していない可能性があることをユーザーに通知します。

オプション 3 - 状態の検証を追加する

データベースの状態を変更するほとんどの操作では、成功したかどうかを確認するコードを追加できます。 EF には、これを簡単にするための拡張メソッド IExecutionStrategy.ExecuteInTransaction が用意されています。

このメソッドは、トランザクションを開始してコミットします。また、トランザクションのコミット中に一時的なエラーが発生したときに呼び出される verifySucceeded パラメーターの関数も受け入れます。


using var db = new BloggingContext();
var strategy = db.Database.CreateExecutionStrategy();

var blogToAdd = new Blog { Url = "http://blogs.msdn.com/dotnet" };
db.Blogs.Add(blogToAdd);

strategy.ExecuteInTransaction(
    db,
    operation: context => { context.SaveChanges(acceptAllChangesOnSuccess: false); },
    verifySucceeded: context => context.Blogs.AsNoTracking().Any(b => b.BlogId == blogToAdd.BlogId));

db.ChangeTracker.AcceptAllChanges();

Note

SaveChanges が成功した場合に Blog エンティティの状態が Unchanged に変更されないようにするため、acceptAllChangesOnSuccessfalse に設定されて SaveChanges が呼び出されます。 これにより、コミットが失敗してトランザクションがロールバックされた場合に、同じ操作を再試行することができます。

方法 4 - トランザクションを手動で追跡する

ストア生成キーを使用する必要がある場合、または実行された操作に依存しないコミット エラーを処理する汎用的な方法が必要な場合は、コミットが失敗したときに検査される ID を、各トランザクションに割り当てることができます。

  1. トランザクションの状態を追跡するために使用するデータベースにテーブルを追加します。
  2. 各トランザクションの開始時に、そのテーブルに行を挿入します。
  3. コミット中に接続エラーが発生した場合は、対応する行がデータベースに存在するかどうかをチェックします。
  4. コミットが成功した場合は、テーブルの肥大化を防ぐために、対応する行を削除します。

using var db = new BloggingContext();
var strategy = db.Database.CreateExecutionStrategy();

db.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/dotnet" });

var transaction = new TransactionRow { Id = Guid.NewGuid() };
db.Transactions.Add(transaction);

strategy.ExecuteInTransaction(
    db,
    operation: context => { context.SaveChanges(acceptAllChangesOnSuccess: false); },
    verifySucceeded: context => context.Transactions.AsNoTracking().Any(t => t.Id == transaction.Id));

db.ChangeTracker.AcceptAllChanges();
db.Transactions.Remove(transaction);
db.SaveChanges();

Note

検証に使用されるコンテキストに、実行戦略が定義されていることを確認します。トランザクションのコミット中に失敗した場合、検証中に接続が再び失敗する可能性があるためです。

その他のリソース