處理並行衝突 (EF6)

開放式平行存取牽涉到開放式嘗試將實體儲存至資料庫,希望自載入實體之後,該處的資料尚未變更。 如果事實證明資料已變更,則會擲回例外狀況,而且您必須先解決衝突,再嘗試儲存一次。 本主題涵蓋如何在 Entity Framework 中處理這類例外狀況。 本主題所示範的技巧同樣適用於使用 Code First 和 EF 設計工具所建立的模型。

此文章不適合完整討論開放式平行存取。 下列各節假設有一些並行解析知識,並顯示常見工作的模式。

其中許多模式都使用使用屬性值 中所 討論的主題。

當您使用獨立關聯時解決並行問題(其中外鍵未對應至實體中的屬性),比使用外鍵關聯時要困難得多。 因此,如果您要在應用程式中執行並行解析,建議您一律將外鍵對應至您的實體。 下列所有範例都假設您使用外鍵關聯。

當偵測到開放式平行存取例外狀況時,SaveChanges 會在嘗試儲存使用外鍵關聯之實體時擲回 DbUpdateConcurrencyException。

使用 Reload 解決開放式平行存取例外狀況 (資料庫獲勝)

Reload 方法可用來以目前資料庫中的值覆寫實體的目前值。 然後實體通常會以某種形式傳回給使用者,他們必須嘗試再次進行變更並重新儲存。 例如:

using (var context = new BloggingContext())
{
    var blog = context.Blogs.Find(1);
    blog.Name = "The New ADO.NET Blog";

    bool saveFailed;
    do
    {
        saveFailed = false;

        try
        {
            context.SaveChanges();
        }
        catch (DbUpdateConcurrencyException ex)
        {
            saveFailed = true;

            // Update the values of the entity that failed to save from the store
            ex.Entries.Single().Reload();
        }

    } while (saveFailed);
}

模擬並行例外狀況的好方法是在 SaveChanges 呼叫上設定中斷點,然後使用 SQL Server Management Studio 等其他工具修改資料庫中儲存的實體。 您也可以在 SaveChanges 之前插入一行,以使用 SqlCommand 直接更新資料庫。 例如:

context.Database.SqlCommand(
    "UPDATE dbo.Blogs SET Name = 'Another Name' WHERE BlogId = 1");

DbUpdateConcurrencyException 上的 Entries 方法會傳回無法更新之實體的 DbEntityEntry 實例。 (此屬性目前一律會針對並行問題傳回單一值。它可能會傳回一般更新例外狀況的多個值。某些情況的替代方案可能是取得可能需要從資料庫重載的所有實體專案,並針對每個實體呼叫重載。

解決用戶端獲勝時開放式平行存取例外狀況

上述使用 Reload 的範例有時稱為資料庫 wins 或 store wins,因為實體中的值會由來自資料庫的值覆寫。 有時候,您可能想要執行相反動作,並以實體中目前的值覆寫資料庫中的值。 這有時稱為用戶端獲勝,而且可以藉由取得目前的資料庫值,並將其設定為實體的原始值來完成。 (請參閱 使用屬性值 以取得目前和原始值的相關資訊。例如:

using (var context = new BloggingContext())
{
    var blog = context.Blogs.Find(1);
    blog.Name = "The New ADO.NET Blog";

    bool saveFailed;
    do
    {
        saveFailed = false;
        try
        {
            context.SaveChanges();
        }
        catch (DbUpdateConcurrencyException ex)
        {
            saveFailed = true;

            // Update original values from the database
            var entry = ex.Entries.Single();
            entry.OriginalValues.SetValues(entry.GetDatabaseValues());
        }

    } while (saveFailed);
}

開放式平行存取例外狀況的自訂解析

有時候,您可能想要結合資料庫中目前的值與實體中目前的值。 這通常需要一些自訂邏輯或使用者互動。 例如,您可以將表單呈現給使用者,其中包含目前值、資料庫中的值,以及一組預設的已解析值。 然後,使用者會視需要編輯已解析的值,而這些解析的值會儲存至資料庫。 這可以使用實體專案上從 CurrentValues 和 GetDatabaseValues 傳回的 DbPropertyValues 物件來完成。 例如:

using (var context = new BloggingContext())
{
    var blog = context.Blogs.Find(1);
    blog.Name = "The New ADO.NET Blog";

    bool saveFailed;
    do
    {
        saveFailed = false;
        try
        {
            context.SaveChanges();
        }
        catch (DbUpdateConcurrencyException ex)
        {
            saveFailed = true;

            // Get the current entity values and the values in the database
            var entry = ex.Entries.Single();
            var currentValues = entry.CurrentValues;
            var databaseValues = entry.GetDatabaseValues();

            // Choose an initial set of resolved values. In this case we
            // make the default be the values currently in the database.
            var resolvedValues = databaseValues.Clone();

            // Have the user choose what the resolved values should be
            HaveUserResolveConcurrency(currentValues, databaseValues, resolvedValues);

            // Update the original values with the database values and
            // the current values with whatever the user choose.
            entry.OriginalValues.SetValues(databaseValues);
            entry.CurrentValues.SetValues(resolvedValues);
        }
    } while (saveFailed);
}

public void HaveUserResolveConcurrency(DbPropertyValues currentValues,
                                       DbPropertyValues databaseValues,
                                       DbPropertyValues resolvedValues)
{
    // Show the current, database, and resolved values to the user and have
    // them edit the resolved values to get the correct resolution.
}

使用 物件自訂開放式平行存取例外狀況的解析

上述程式碼會使用 DbPropertyValues 實例來傳遞目前、資料庫和已解析的值。 有時候,使用實體類型的實例可能比較容易。 這可以使用 DbPropertyValues 的 ToObject 和 SetValues 方法來完成。 例如:

using (var context = new BloggingContext())
{
    var blog = context.Blogs.Find(1);
    blog.Name = "The New ADO.NET Blog";

    bool saveFailed;
    do
    {
        saveFailed = false;
        try
        {
            context.SaveChanges();
        }
        catch (DbUpdateConcurrencyException ex)
        {
            saveFailed = true;

            // Get the current entity values and the values in the database
            // as instances of the entity type
            var entry = ex.Entries.Single();
            var databaseValues = entry.GetDatabaseValues();
            var databaseValuesAsBlog = (Blog)databaseValues.ToObject();

            // Choose an initial set of resolved values. In this case we
            // make the default be the values currently in the database.
            var resolvedValuesAsBlog = (Blog)databaseValues.ToObject();

            // Have the user choose what the resolved values should be
            HaveUserResolveConcurrency((Blog)entry.Entity,
                                       databaseValuesAsBlog,
                                       resolvedValuesAsBlog);

            // Update the original values with the database values and
            // the current values with whatever the user choose.
            entry.OriginalValues.SetValues(databaseValues);
            entry.CurrentValues.SetValues(resolvedValuesAsBlog);
        }

    } while (saveFailed);
}

public void HaveUserResolveConcurrency(Blog entity,
                                       Blog databaseValues,
                                       Blog resolvedValues)
{
    // Show the current, database, and resolved values to the user and have
    // them update the resolved values to get the correct resolution.
}