處理並行存取衝突Handling Concurrency Conflicts

注意

此頁面會記載在 EF 核心中的並行存取如何運作以及如何處理您的應用程式中的並行存取衝突。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 核心實作_開放式並行存取控制_,也就是說,它會讓多個處理序或使用者進行變更,無同步處理的額外負荷,或鎖定。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 核心中的並行存取控制如何運作How concurrency control works in EF Core

屬性設定為並行語彙基元可用於實作開放式並行存取控制: 每當執行 update 或 delete 作業期間SaveChanges,會比對原始資料庫上的並行語彙基元值EF 核心所讀取的值。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 核心會假設其他使用者已執行衝突的作業,並中止目前交易。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 核心包含值的並行語彙基元中的核取WHERE的任何子句UPDATEDELETE陳述式。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 核心會讀取受影響的資料列數目。After executing the statements, EF Core reads the number of rows that were affected.

如果沒有資料列受到影響,偵測到並行衝突時,並 EF 核心擲回DbUpdateConcurrencyExceptionIf no rows are affected, a concurrency conflict is detected, and EF Core throws DbUpdateConcurrencyException.

例如,我們可能會想要設定LastNamePerson是並行語彙基元。For example, we may want to configure LastName on Person to be a concurrency token. 則人員的任何更新作業將會包含在並行存取檢查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. 攔截DbUpdateConcurrencyException期間SaveChangesCatch 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);
                }
            }
        }
    }
}