如何在实体框架中更新大型对象

本文关键字:更新 大型 对象 框架 实体 | 更新日期: 2023-09-27 18:29:49

在我的项目中,我有一个实体模型,看起来像:

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和所有OrderPosition(插入新的、更新现有的和删除旧的)。我尝试了几种方法,但都失败了:

  1. 删除订单和订单位置,然后插入新的失败
  2. 重复键异常。Detatch订单和附件更新-失败,原因为:

附加"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();
    }
}

如何在实体框架中更新大型对象

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

SQL Server在每次更新时都会检查唯一性,而不是在提交时。由于重新排序会临时创建非唯一。。。。好

你需要两次传球。先把东西移开,然后更新到最终位置。

如何?SQL Server数据类型支持负数;)把它们放在负数的位置(即-4而不是4),然后你就可以为客户制作一个反转负数的SP(或直接SQL)。完成。

但在更新过程中,您需要打破唯一性。

唉,当涉及到这种类型的操作时,实体框架非常愚蠢,它为您提供了框架,而不是一个完整的解决方案。

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

它不能在一次调用SaveChanges中实现的原因是,没有办法告诉实体框架你想要的操作顺序,即它可能会在更改外键等之前删除一个表记录。

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

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将被回滚/不执行操作。

当对SaveChanges(false)的调用将必要的命令发送到数据库时,上下文本身不会更改,因此如果需要,您可以再次执行此操作,或者如果需要,可以询问ObjectStateManager。

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

要扩展我上面的评论。。。

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

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

你在这里应该做的是。。。

    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()