【问题标题】:How to update large object in Entity Framework如何在实体框架中更新大对象
【发布时间】:2014-11-11 00:40:15
【问题描述】:

在我的项目中,我的实体模型如下所示:

public class OrdersDbContext : DbContext
{
    public DbSet<Customer> Customers { get; set; }
    public DbSet<Order> Orders { get; set; }
    public DbSet<OrderPosition> OrderPositions { get; set; }
}

public class Customer
{
    public Guid Id { get; set; }
    public virtual Order LiveOrder { get; set; }
}

public class Order
{
    public Guid Id { get; set; }
    public virtual IList<OrderPosition> Positions { get; set; }
    public string Name { get; set; }
}

public class OrderPosition
{
    public Guid Id { get; set; }
    public int Price { get; set; }
}

我需要提供一种方法来更新一些客户的属性以及他的 LiveOrder 和所有 OrderPositions(插入新的、更新现有的和删除旧的)。我尝试了几种方法,但都失败了:

  1. 删除订单和订单仓位,然后插入新的失败,原因是
  2. 重复键异常。分离订单,然后附加更新 - 失败:

附加类型为“OrderPosition”的实体失败,因为另一个 相同类型的实体已经具有相同的主键值。

正确的做法是什么?

演示问题的完整控制台程序:

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
public class OrdersDbContext : DbContext
{
    public DbSet<Customer> Customers { get; set; }
    public DbSet<Order> Orders { get; set; }
    public DbSet<OrderPosition> OrderPositions { get; set; }
}

public class Customer
{
    public Guid Id { get; set; }
    public virtual Order LiveOrder { get; set; }
}

public class Order
{
    public Guid Id { get; set; }
    public virtual IList<OrderPosition> Positions { get; set; }
    public string Name { get; set; }
}

public class OrderPosition
{
    public Guid Id { get; set; }
    public int Price { get; set; }
}

class Program
{
    static void Main(string[] args)
    {
        var parentId = Guid.NewGuid();
        var childId = Guid.NewGuid();

        using (var ctx = new OrdersDbContext())
        {
            var agg = new Customer{ 
                Id = Guid.NewGuid(),
                LiveOrder = new Order
            {
                Id = parentId,
                Name = "First order.",
                Positions = new[] { new OrderPosition { Id = childId, Price = 3 } }
            }};
            ctx.Customers.Add(agg);
            ctx.SaveChanges();
        }

        var newParent = new Order
        {
            Id = parentId,
            Name = "Updated order.",
            Positions = new[] { new OrderPosition { Id = childId, Price = 5 } }
        };
        try
        {
            using (var ctx = new OrdersDbContext())
            {
                var agg = ctx.Customers.First(x => x.LiveOrder.Id == parentId);

                ctx.OrderPositions.RemoveRange(agg.LiveOrder.Positions);
                var parent = agg.LiveOrder;
                agg.LiveOrder = null;
                ctx.Entry(parent).State = EntityState.Detached;
                Console.WriteLine(ctx.Entry(parent).State);
                ctx.Entry(newParent).State = EntityState.Modified;
                agg.LiveOrder = newParent;

                ctx.SaveChanges();
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex);
        }
        Console.WriteLine("Press any key to exit.");
        Console.ReadLine();
    }
}

【问题讨论】:

  • 据我所知,实体不会批量更新/插入,因此如果您更改订单位置,您将始终得到重复值。我见过的一种方法是,您在订单中使用浮点数,随机生成分数,同时仍保持整数部分递增
  • 在删除 Order 和 OrderPositions 而不是插入它之后,我设法用额外的 SaveChanges 对其进行了更新。但我认为应该有更好的方法来使用单个 SaveChanges。我想我也可以分离所有 OrderPosition,而不是附加新的/更新的。但在这种情况下,我填写就像我在做 ERM 的工作。
  • 我将我的评论复制到一个答案中,因为我觉得它提供了一种新的方式。
  • 第一点...在事务中执行!...如果您的某个更改失败怎么办?但其他一些人成功了吗?数据损坏?
  • 为什么在agg.LiveOrder = null; 之后不调用保存更改?数据库将无法处理。您正在从数据库中分离实体存储并在实体存储中创建一个新对象,该对象不会链接到数据库中的对象。您在这里遇到的问题是该行仍然存在于数据库中,在客户端您正在删除它,然后告诉实体存储忘记它,然后向实体存储添加一个新的,然后提交它。这将使 EF 执行插入而不是更新,相同的 Guid 会导致重复键错误。

标签: c# entity-framework-6


【解决方案1】:

这完全不是 EF 问题 - 使用纯 SQL 会遇到同样的问题。

SQL Server 在每次更新时检查唯一性,而不是在提交时检查。作为重新排序暂时创建非唯一......好吧......

您需要 2 次通行证。先把东西移开,然后更新到最终位置。

怎么样?好吧,SQL Server 数据类型支持负数 ;) 将它们放在它们的位置,负数(即 -4 而不是 4),然后你可以为客户创建一个反转负数的 SP(或直接 SQL)。完成了。

但您需要在更新期间打破唯一性。

【讨论】:

  • +1 我喜欢这个答案,它还允许您继续使用整数类型来存储值。
  • 再一次,问题不在于重新排序。我的意思是这里的采购订单。
【解决方案2】:

唉,实体框架在这种类型的操作方面相当愚蠢,它给你的是框架,而不是一个完整的解决方案。

正如你所做的那样;这可以通过多次调用 SaveChanges 来实现。

一次调用 SaveChanges 无法实现它的原因是,无法告诉 Entity Framework 您希望执行操作的顺序,即它可能会在更改外键之前删除一个表记录等。

现在执行多个保存更改的更好方法是在事务中如下:

TransactionScope scope = new TransactionScope(TransactionScopeOption.RequiresNew, new TransactionOptions() { IsolationLevel = IsolationLevel.Serializable });

try
{
    using (scope)
    {
        using (OrdersDbContext ctx = new OrdersDbContext())
        {
            ctx.Connection.Open();

            //Do something with DBcontext - Update existing
            //Save Changes
            ctx.SaveChanges(false);

            //Do something else with DBcontext - Delete old
            //Save Changes
            ctx.SaveChanges(false);

            //Do something else with DBcontext - Insert new
            //Save Changes
            ctx.SaveChanges(false);

            //if we get here things are looking good.
            scope.Complete();
            ctx.AcceptAllChanges();
        }

        scope.Complete();
    }
}
catch (TransactionAbortedException ex)
{
    ErrorLogger.LogException();
}
catch (ApplicationException ex)
{
    ErrorLogger.LogException();
}

这里的主要好处是,如果一个 SaveChanges 失败,那么其他的会回滚/不执行。

虽然对 SaveChanges(false) 的调用将必要的命令发送到数据库,但上下文本身并未更改,因此您可以在必要时再次执行此操作,或者您可以根据需要询问 ObjectStateManager。

这意味着如果事务实际中止,您可以通过在某处重试或记录上下文 ObjectStateManager 的状态来进行补偿。

【讨论】:

  • 不是 OP,但你能解释更多/链接解释你在这里做什么,作为一个感兴趣的旁观者,我实际上并不了解正在发生的事情或这两种情况可能相关到。
  • 好的...那么就是延迟提交吗?
  • 更多的是关于准确指定可能破坏主/外键关系等的操作何时发生......所以你做事情的顺序不会破坏数据库规则。
  • 如果您在上下文中更改多个实体,然后调用保存更改...如果您所做的某些更改会破坏数据库,那么您很幸运如果它有效...一旦您调用 SaveChanges EF将订购实际的数据库更改,但它喜欢出于优化目的我相信......所以你必须一次做一件事情,以不会破坏数据库的顺序。
【解决方案3】:

在上面扩展我的评论...

数据库和 EF 将无法处理该问题。您正在从数据库中分离实体存储并在实体存储中创建一个新对象,该对象不会链接到数据库中的对象。

您在这里遇到的问题是该行仍然存在于数据库中,在客户端您正在删除它,然后告诉实体存储忘记它,然后向实体存储添加一个新的,然后提交它。这将使 EF 执行插入而不是更新,相同的 Guid 会导致重复键错误。

你应该在这里做的是以下......

    try
    {
        using (var ctx = new OrdersDbContext())
        {
            var agg = ctx.Customers.First(x => x.LiveOrder.Id == parentId);

            var pos = agg.LiveOrder.Single(o => o.Id == childId);
            pos.Price = 5;

            ctx.SaveChanges();
        }
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex);
    }

实体在客户端存储对象,并将检测对它们的更改。您所要做的就是像普通对象一样修改它们并调用SaveChanges()

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-05-19
    • 1970-01-01
    • 1970-01-01
    • 2013-07-15
    相关资源
    最近更新 更多