同時実行の競合の処理Handling Concurrency Conflicts

注意

このページは、EF Core での同時実行のしくみと、アプリケーションで同時実行の競合を処理する方法について説明します。This page documents how concurrency works in EF Core and how to handle concurrency conflicts in your application. モデルで同時実行トークンを構成する方法の詳細については、同時実行トークンに関するページを参照してください。See Concurrency Tokens for details on how to configure concurrency tokens in your model.

ヒント

この記事のサンプルは GitHub で確認できます。You can view this article's sample on GitHub.

"データベースの同時実行" とは、複数のプロセスまたはユーザーが、データベース内の同じデータに対して同時にアクセスするか、または変更を加える状況のことです。Database concurrency refers to situations in which multiple processes or users access or change the same data in a database at the same time. "同時実行の制御" とは、同時実行の変更においてデータの一貫性を保証するために使用される特定のメカニズムのことです。Concurrency control refers to specific mechanisms used to ensure data consistency in presence of concurrent changes.

EF Core は、"オプティミスティック同時実行制御" を実装しています。これは、同期やロックのオーバーヘッドを伴わずに、複数のプロセスまたはユーザーが独立して変更を加えられることを意味します。EF Core implements optimistic concurrency control, meaning that it will let multiple processes or users make changes independently without the overhead of synchronization or locking. 理想的な状況であれば、これらの変更が互いに干渉しないため、正常に処理できます。In the ideal situation, these changes will not interfere with each other and therefore will be able to succeed. 最悪のケースのシナリオでは、2 つ以上のプロセスが、競合する変更を行おうとして、どちらか一方しか正常に処理されません。In the worst case scenario, two or more processes will attempt to make conflicting changes, and only one of them should succeed.

EF Core での同時実行制御のしくみHow concurrency control works in EF Core

同時実行トークンとして構成されるプロパティは、オプティミスティック同時実行制御の実装に使用されます。更新または削除処理が SaveChanges の間に実行された場合は常に、データベース上の同時実行トークンの値が、EF Core で読み取られた元の値と比較されます。Properties configured as concurrency tokens are used to implement optimistic concurrency control: whenever an update or delete operation is performed during SaveChanges, the value of the concurrency token on the database is compared against the original value read by EF Core.

  • 値が一致した場合、操作は完了できます。If the values match, the operation can complete.
  • 値が一致しない場合、EF Core では、別のユーザーが競合する処理を実行したと仮定して、現在のトランザクションを中断します。If the values do not match, EF Core assumes that another user has performed a conflicting operation and aborts the current transaction.

現在の処理と競合する処理を別のユーザーが実行した場合の状況を、"同時実行の競合" といいます。The situation when another user has performed an operation that conflicts with the current operation is known as concurrency conflict.

データベース プロバイダーは、同時実行トークン値の比較の実装を担います。Database providers are responsible for implementing the comparison of concurrency token values.

リレーショナル データベースでは、EF Core は UPDATEDELETE ステートメントの WHERE 句に、同時実行トークン値のチェックを含みます。On relational databases EF Core includes a check for the value of the concurrency token in the WHERE clause of any UPDATE or DELETE statements. ステートメントを実行した後、EF Core は、影響を受けた行数を読み取ります。After executing the statements, EF Core reads the number of rows that were affected.

影響を受けた行がない場合、同時実行の競合が検出され、EF Core は DbUpdateConcurrencyException をスローします。If no rows are affected, a concurrency conflict is detected, and EF Core throws DbUpdateConcurrencyException.

たとえば、Person 上で同時実行トークンになるように、LastName を構成できます。For example, we may want to configure LastName on Person to be a concurrency token. これにより、Person 上での更新処理に、WHERE 句での同時実行のチェックが含まれるようになります。Then any update operation on Person will include the concurrency check in the WHERE clause:

UPDATE [Person] SET [FirstName] = @p1
WHERE [PersonId] = @p0 AND [LastName] = @p2;

同時実行の競合の解決Resolving concurrency conflicts

前の例に続けて、あるユーザーが Person に対するいくつかの変更を保存しようとしたところ、別のユーザーが既に LastName を変更していた場合、例外がスローされます。Continuing with the previous example, if one user tries to save some changes to a Person, but another user has already changed the LastName the an exception will be thrown.

この時点で、アプリケーションは単純に、変更の競合が原因で更新が成功しなかったことをユーザーに通知して、次の処理へ移ります。At this point, the application could simply inform the user that the update was not successful due to conflicting changes and move on. しかし、このレコードが引き続き実際の同一人物を表しており、処理を再試行できることを確認するよう、ユーザーに求めることが望ましい場合があります。But it may be desirable to prompt the user to ensure this record still represents the same actual person and to retry the operation.

このプロセスは、"同時実行の競合の解決" の例です。This process is an example of resolving a concurrency conflict.

同時実行の競合を解決するには、現在の DbContext からの保留中の変更をデータベースの値とマージします。Resolving a concurrency conflict involves merging the pending changes from the current DbContext with the values in the database. どの値がマージされるかは、アプリケーションに基づいて変わります。また、ユーザー入力によって指示することが可能です。What values get merged will vary based on the application and may be directed by user input.

同時実行の競合を解決するために使用可能な 3 つの設定値を次に示します。There are three sets of values available to help resolve a concurrency conflict:

  • 現在の値は、アプリケーションがデータベースへの書き込みを試行している値です。Current values are the values that the application was attempting to write to the database.

  • 元の値は、何らかの編集が行われる前に、データベースから取得された元の値です。Original values are the values that were originally retrieved from the database, before any edits were made.

  • データベース値は、データベースに現在格納されている値です。Database values are the values currently stored in the database.

同時実行の競合を処理する一般的な方法は、次のとおりです。The general approach to handle a concurrency conflicts is:

  1. SaveChanges の間に DbUpdateConcurrencyException をキャッチします。Catch DbUpdateConcurrencyException during SaveChanges.
  2. DbUpdateConcurrencyException.Entries を使用して、影響を受けるエンティティに対する新しい変更の設定を準備します。Use DbUpdateConcurrencyException.Entries to prepare a new set of changes for the affected entities.
  3. 同時実行トークンの元の値を更新して、データベースの現在の値を反映します。Refresh the original values of the concurrency token to reflect the current values in the database.
  4. 競合が発生しなくなるまで、プロセスを再試行します。Retry the process until no conflicts occur.

次の例では、Person.FirstNamePerson.LastName は、同時実行トークンとして設定されています。In the following example, Person.FirstName and Person.LastName are setup as concurrency tokens. 保存する値を選択するためのアプリケーション固有のロジックを含める場所に、// TODO: コメントがあります。There is a // TODO: comment in the location where you include application specific logic to choose the value to be saved.

using (var context = new PersonContext())
{
    // Fetch a person from database and change phone number
    var person = context.People.Single(p => p.PersonId == 1);
    person.PhoneNumber = "555-555-5555";

    // Change the person's name in the database to simulate a concurrency conflict
    context.Database.ExecuteSqlCommand(
        "UPDATE dbo.People SET FirstName = 'Jane' WHERE PersonId = 1");

    var saved = false;
    while (!saved)
    {
        try
        {
            // Attempt to save changes to the database
            context.SaveChanges();
            saved = true;
        }
        catch (DbUpdateConcurrencyException ex)
        {
            foreach (var entry in ex.Entries)
            {
                if (entry.Entity is Person)
                {
                    var proposedValues = entry.CurrentValues;
                    var databaseValues = entry.GetDatabaseValues();

                    foreach (var property in proposedValues.Properties)
                    {
                        var proposedValue = proposedValues[property];
                        var databaseValue = databaseValues[property];

                        // TODO: decide which value should be written to database
                        // proposedValues[property] = <value to be saved>;
                    }

                    // Refresh original values to bypass next concurrency check
                    entry.OriginalValues.SetValues(databaseValues);
                }
                else
                {
                    throw new NotSupportedException(
                        "Don't know how to handle concurrency conflicts for "
                        + entry.Metadata.Name);
                }
            }
        }
    }
}