如何处理在 Entity Framework 6 中具有具有相同 PK 的重复实体的分离实体图

How to handle a detached entity graph that has duplicate entities with the same PK in Entity Framework 6

例如,我正在创建发票图表..

该图是用 AnuglarJS 在客户端构建的,然后作为 JSON 发送到后端。在后端,我收到一个看似完美的对象,它是由 Web 中的一些模型绑定魔术创建的 Api 2.

接下来我从 DbContext 加载管理实体并将分离的发票添加到它的集合导航中 属性:

Administration.Lines.Add(invoice);

触发 DetectChanges 时,图中的每个实体都会获得一个状态为 EntityState.Added 的条目。

从图表中我只想添加实体 Invoice 和 InvoiceLines,因为图表的其余部分没有改变。为了完成这项工作,我采用了在本地跟踪状态的方法,例如使用实现状态 属性 的 BaseModel 接口(如编程 Entity Framework:DbContext,第 4 章中所建议)。

这一切都很好地工作...直到例如,当我添加 2 行相同的项目时。出于说明目的,我相信模型联编程序从 JSON 创建对象有点像:

var invoice = new Invoice
{
    State = State.Added,
    Number = "SALE-001",
    Lines =
    {
        new InvoiceLine
        {
            State = State.Added,
            Price = 10,
            Quantity = 1,
            Item = new Item { Id = 1, Description = "Product" }
        },
        new InvoiceLine
        {
            State = State.Added,
            Price = 10,
            Quantity = 1,
            DiscountPercentage = 50,
            Item = new Item { Id = 1, Description = "Product" }
        },
    }
};

如您所见,这些项目共享相同的 PK,但实际上是不同的对象。因此,当 DetectChanges 被触发时,我有 2 个具有相同 PK 的实体 Item 条目,当我尝试使用 InvalidOperationException 将状态 Added 转换为 Unchanged 时,这一切都爆炸了:

Saving or accepting changes failed because more than one entity of type 'AutomapperWithEF.Data.Domain.Item' have the same primary key value. Ensure that explicitly set primary key values are unique. Ensure that database-generated primary keys are configured correctly in the database and in the Entity Framework model. Use the Entity Designer for Database First/Model First configuration. Use the 'HasDatabaseGeneratedOption" fluent API or 'DatabaseGeneratedAttribute' for Code First configuration.

EF 不应该以某种方式阻止这种情况的发生吗?通过相同类型的相等比较和PK?

编辑:

DbContext,其中 ChangeTracker.Entries 是触发器:

public class AppContext : DbContext
{
    public DbSet<Administration> Administrations { get; set; }
    public DbSet<Invoice> Invoices { get; set; }
    public DbSet<InvoiceLine> InvoiceLines { get; set; }
    public DbSet<Item> Items { get; set; }

    public override int SaveChanges()
    {
        foreach (var entry in ChangeTracker.Entries<BaseModel>())
        {
            BaseModel baseModel = entry.Entity;
            entry.State = ConvertState(baseModel.State);
        }

        return base.SaveChanges();
    }

    public static EntityState ConvertState(State state)
    {
        switch (state)
        {
            case State.Added:
                return EntityState.Added;
            case State.Modified:
                return EntityState.Modified;
            case State.Deleted:
                return EntityState.Detached;
            default:
                return EntityState.Unchanged;
        }
    }
}

一个例子来说明我的意思:https://github.com/svieillard/SampleEF

您遇到此问题的原因是您正在创建两个不同的 Item 对象,为它们提供相同的键,并试图将它们都添加到上下文中。

Item = new Item { Id = 1, Description = "Product" }

/// and later

Item = new Item { Id = 1, Description = "Product" }

调用 .Add() 时会发生异常,因为在 EF 上下文中不能有两个具有相同键的相同类型的不同对象。

当我查看您的 github 存储库中的代码时,您似乎已经解决了这个问题。我所要做的就是注释掉你当前如何创建 Items 并取消注释你的其他代码,结果是:

var item = new Item { Id = 1, Description = "Product" };

// Invoice is added
var invoice = new Invoice
{
    State = State.Added,
    Number = "SALE-001",
    Lines =
    {
        new InvoiceLine
        {
            State = State.Added,
            Price = 10,
            Quantity = 1,
            // Item = new Item { Id = 1, Description = "Product" }
            Item = item
        },
        new InvoiceLine
        {
            State = State.Added,
            Price = 10,
            Quantity = 1,
            DiscountPercentage = 50,
            // Item = new Item { Id = 1, Description = "Product" }
            Item = item
        },
    }
};

这样,只有 1 个 Item 最终被添加到上下文中,所以 EF 很高兴。

当我 运行 这样的代码时,它可以正常工作,不会抛出任何异常。

您正在将一些 JSON 反序列化为一个 Invoice,它有两个具有相同 ID 的 Item 对象。然后你 Add()Invoice 到上下文然后调用 SaveChanges().

当您执行此操作时,EF 会发现您两次将项目放入其中并抛出异常。

即使您只有 1 个项目,EF 也会尝试根据 JSON 中的数据更新项目。你可能不想要那个。

所以我们要从上下文中删除项目。

解决方案 #1:从内存中的对象中删除项目并仅使用 ItemId

foreach (var line in invoice.Lines)
{
    if (line != null && line.Item != null)
    {
        line.ItemId = line.Item.Id;
        db.Entry(line.Item).State = System.Data.Entity.EntityState.Detached;
    }
}

解决方案 #2 - 更改您的 JSON 行项目以使用 ItemId 而不是 Item,就像这样

{'state': 2, 'price': 10, 'quantity': 1, 'itemId': 1}