處理並行存取衝突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. 在最糟的情況下,會有兩個或更多個處理程序嘗試進行衝突變更,而只有其中一個應該會成功。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 就會擲回 DbUpdateConcurrencyExceptionIf 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.

有三組值可供協助解決並行存取衝突: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 期間攔截 DbUpdateConcurrencyExceptionCatch 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);
                }
            }
        }
    }
}