处理并发冲突 (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 的示例有时被称为数据库优先或存储优先,因为实体中的值由数据库中的值覆盖。 有时,你可能希望执行相反的操作,并用实体中当前的值覆盖数据库中的值。 这种情况有时称为客户端优先,可通过获取当前数据库值并将其设置为实体的原始值来完成。 (有关当前值和原始值的信息,请参阅使用属性值。)例如:

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.
}