處理並行Handling Concurrency

如果屬性設定為並行語彙基元 EF 會檢查沒有其他使用者在儲存變更至該記錄時,已修改該資料庫中的值。If a property is configured as a concurrency token then EF will check that no other user has modified that value in the database when saving changes to that record.

提示

您可以檢視這篇文章範例GitHub 上。You can view this article's sample on GitHub.

在 EF 核心中的並行處理如何運作How concurrency handling works in EF Core

如需 Entity Framework Core 中並行處理的運作方式的詳細說明,請參閱並行語彙基元For a detailed description of how concurrency handling works in Entity Framework Core, see Concurrency Tokens.

解決並行衝突Resolving concurrency conflicts

解決並行衝突,包括使用演算法來合併資料庫中所作的變更與目前使用者的暫止變更。Resolving a concurrency conflict involves using an algorithm to merge the pending changes from the current user with the changes made in the database. 確切方式會因您的應用程式,但常見的作法是將值顯示給使用者,並請他們決定正確的值儲存在資料庫中。The exact approach will vary based on your application, but a common approach is to display the values to the user and have them decide the correct values to be stored in the database.

有三個設定的值可用來協助解決並行衝突。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.

若要處理並行衝突,攔截DbUpdateConcurrencyException期間SaveChanges(),使用DbUpdateConcurrencyException.Entries準備受影響的實體,一組新的變更,然後重試SaveChanges()作業。To handle a concurrency conflict, catch a DbUpdateConcurrencyException during SaveChanges(), use DbUpdateConcurrencyException.Entries to prepare a new set of changes for the affected entities, and then retry the SaveChanges() operation.

在下列範例中,Person.FirstNamePerson.LastName已設定為並行語彙基元。In the following example, Person.FirstName and Person.LastName are setup as concurrency token. 沒有// TODO:註解中的位置,您會在其中加入應用程式特定的邏輯,以選擇要儲存到資料庫的值。There is a // TODO: comment in the location where you would include application specific logic to choose the value to be saved to the database.

using Microsoft.EntityFrameworkCore;
using System;
using System.ComponentModel.DataAnnotations;
using System.Linq;

namespace EFSaving.Concurrency
{
    public class Sample
    {
        public static void Run()
        {
            // Ensure database is created and has a person in it
            using (var context = new PersonContext())
            {
                context.Database.EnsureDeleted();
                context.Database.EnsureCreated();

                context.People.Add(new Person { FirstName = "John", LastName = "Doe" });
                context.SaveChanges();
            }

            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 persons name in the database (will cause a concurrency conflict)
                context.Database.ExecuteSqlCommand("UPDATE dbo.People SET FirstName = 'Jane' WHERE PersonId = 1");

                try
                {
                    // Attempt to save changes to the database
                    context.SaveChanges();
                }
                catch (DbUpdateConcurrencyException ex)
                {
                    foreach (var entry in ex.Entries)
                    {
                        if (entry.Entity is Person)
                        {
                            // Using a NoTracking query means we get the entity but it is not tracked by the context
                            // and will not be merged with existing entities in the context.
                            var databaseEntity = context.People.AsNoTracking().Single(p => p.PersonId == ((Person)entry.Entity).PersonId);
                            var databaseEntry = context.Entry(databaseEntity);

                            foreach (var property in entry.Metadata.GetProperties())
                            {
                                var proposedValue = entry.Property(property.Name).CurrentValue;
                                var originalValue = entry.Property(property.Name).OriginalValue;
                                var databaseValue = databaseEntry.Property(property.Name).CurrentValue;

                                // TODO: Logic to decide which value should be written to database
                                // entry.Property(property.Name).CurrentValue = <value to be saved>;

                                // Update original values to
                                entry.Property(property.Name).OriginalValue = databaseEntry.Property(property.Name).CurrentValue;
                            }
                        }
                        else
                        {
                            throw new NotSupportedException("Don't know how to handle concurrency conflicts for " + entry.Metadata.Name);
                        }
                    }

                    // Retry the save operation
                    context.SaveChanges();
                }
            }
        }

        public class PersonContext : DbContext
        {
            public DbSet<Person> People { get; set; }

            protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
            {
                optionsBuilder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=EFSaving.Concurrency;Trusted_Connection=True;");
            }
        }

        public class Person
        {
            public int PersonId { get; set; }

            [ConcurrencyCheck]
            public string FirstName { get; set; }

            [ConcurrencyCheck]
            public string LastName { get; set; }

            public string PhoneNumber { get; set; }
        }

    }
}