接続の回復性
接続の回復性により、失敗したデータベース コマンドが自動的に再試行されます。 この機能は、エラーの検出とコマンドの再試行に必要なロジックをカプセル化する "実行戦略" を提供することで、任意のデータベースで使用できます。 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()));
}
手記
エラー時に再試行を有効にすると、EF によって結果セットが内部的にバッファーされるため、大きな結果セットを返すクエリのメモリ要件が大幅に増加する可能性があります。 詳しくは、「バッファーリングとストリーミング」をご覧ください。
カスタム実行戦略
既定値のいずれかを変更する場合は、独自のカスタム実行戦略を登録するメカニズムがあります。
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder
.UseMyProvider(
"<connection string>",
options => options.ExecutionStrategy(...));
}
実行戦略とトランザクション
失敗時に自動的に再試行する実行戦略では、失敗した再試行ブロック内の各操作を再生できる必要があります。 再試行が有効になっている場合、EF Core 経由で実行する各操作は、独自の再試行可能な操作になります。 つまり、一時的な障害が発生した場合、各クエリと SaveChangesAsync()
の各呼び出しがユニットとして再試行されます。
ただし、コードが BeginTransactionAsync()
を使用してトランザクションを開始する場合、それはユニットとして扱う必要がある独自の操作グループを定義することを意味します。エラーが発生した場合は、トランザクション内のすべての操作を再実行する必要があります。 実行戦略を使用するときにこれを行おうとすると、次のような例外が発生します。
InvalidOperationException: 構成された実行戦略 'SqlServerRetryingExecutionStrategy' は、ユーザーが開始したトランザクションをサポートしていません。 "DbContext.Database.CreateExecutionStrategy()" によって返される実行戦略を使用して、トランザクション内のすべての操作を再試行可能な単位として実行します。
解決策は、実行する必要があるすべてを表すデリゲートを使用して、実行戦略を手動で呼び出す方法です。 一時的なエラーが発生した場合、実行戦略によってデリゲートが再度呼び出されます。
using var db = new BloggingContext();
var strategy = db.Database.CreateExecutionStrategy();
await strategy.ExecuteAsync(
async () =>
{
using var context = new BloggingContext();
await using var transaction = await context.Database.BeginTransactionAsync();
context.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/dotnet" });
await context.SaveChangesAsync();
context.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/visualstudio" });
await context.SaveChangesAsync();
await transaction.CommitAsync();
});
この方法は、アンビエント トランザクションでも使用できます。
using var context1 = new BloggingContext();
context1.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/visualstudio" });
var strategy = context1.Database.CreateExecutionStrategy();
await strategy.ExecuteAsync(
async () =>
{
using var context2 = new BloggingContext();
using var transaction = new TransactionScope();
context2.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/dotnet" });
await context2.SaveChangesAsync();
await context1.SaveChangesAsync();
transaction.Complete();
});
トランザクションコミットの失敗と冪等性の問題
一般に、接続エラーが発生した場合、現在のトランザクションはロールバックされます。 ただし、トランザクションのコミット中に接続が切断された場合、トランザクションの結果の状態は不明です。
既定では、実行戦略はトランザクションがロールバックされたかのように操作を再試行しますが、そうでない場合は、新しいデータベースの状態に互換性がない場合や、操作が特定の状態に依存しない場合 (たとえば、自動生成されたキー値を持つ新しい行を挿入する場合など) に データ破損 が発生する可能性がある場合は、例外が発生します。
これに対処するには、いくつかの方法があります。
オプション 1 - 何もしない (ほとんど)
トランザクションのコミット中に接続エラーが発生する可能性は低いので、この状態が実際に発生した場合にアプリケーションが失敗する可能性があります。
ただし、重複する行を追加するのではなく、例外がスローされるようにするために、ストア生成キーを使用しないようにする必要があります。 クライアントによって生成される GUID 値またはクライアント側の値ジェネレーターの使用を検討してください。
オプション 2 - アプリケーションの状態を再構築する
- 現在の
DbContext
を破棄します。 - 新しい
DbContext
を作成し、データベースからアプリケーションの状態を復元します。 - 最後の操作が正常に完了しなかった可能性があることをユーザーに通知します。
オプション 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);
await strategy.ExecuteInTransactionAsync(
db,
operation: (context, cancellationToken) => context.SaveChangesAsync(acceptAllChangesOnSuccess: false, cancellationToken),
verifySucceeded: (context, cancellationToken) => context.Blogs.AsNoTracking().AnyAsync(b => b.BlogId == blogToAdd.BlogId, cancellationToken));
db.ChangeTracker.AcceptAllChanges();
手記
ここでは、SaveChanges
が成功した場合に Blog
エンティティの状態が Unchanged
に変更されないようにするため、acceptAllChangesOnSuccess
を false
に設定して SaveChanges
が呼び出されています。 これにより、コミットが失敗し、トランザクションがロールバックされた場合に、同じ操作を再試行できます。
オプション 4 - トランザクションを手動で追跡する
ストアで生成されたキーを使用する必要がある場合、または各トランザクションを実行した操作に依存しないコミット エラーを処理する一般的な方法が必要な場合は、コミットが失敗したときにチェックされる ID を割り当てることができます。
- トランザクションの状態を追跡するために使用するテーブルをデータベースに追加します。
- 各トランザクションの開始時に、そのテーブルに行を挿入します。
- コミット中に接続が失敗した場合は、対応する行がデータベースに存在するかどうかを確認します。
- コミットが成功した場合は、テーブルの増加を回避するために、対応する行を削除します。
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);
await strategy.ExecuteInTransactionAsync(
db,
operation: (context, cancellationToken) => context.SaveChangesAsync(acceptAllChangesOnSuccess: false, cancellationToken),
verifySucceeded: (context, cancellationToken) => context.Transactions.AsNoTracking().AnyAsync(t => t.Id == transaction.Id, cancellationToken));
db.ChangeTracker.AcceptAllChanges();
db.Transactions.Remove(transaction);
await db.SaveChangesAsync();
手記
トランザクションのコミット中に失敗した場合、検証中に接続が再び失敗する可能性があるため、検証に使用されるコンテキストに実行戦略が定義されていることを確認します。
その他のリソース
- Azure SQL Database と SQL Managed Instance での一時的な接続エラーのトラブルシューティング
.NET