具有多个相同类型的 Include 语句的奇怪实体框架错误

本文关键字:实体 框架 错误 语句 Include 同类型 | 更新日期: 2023-09-27 18:36:25

我使用的是最新版本的实体框架(6.1.3),我有以下类,它允许更改客户名称:

public class CustomerService
{
    public void ChangeName()
    {
        using (TestModel ctx = new TestModel())
        {
            var customer = GetCustomer(ctx);
            //set new name
            customer.Name = "New Name";
            SaveCustomer(ctx, customer);
        }
    }
    private Customer GetCustomer(TestModel ctx)
    {
        //get customer by email
        var customer = ctx.Customers
                          .AsNoTracking()
                          .Include(n => n.Country) //Load Shipping Country
                          .Include(n => n.Country1) //Load Billing Country
                          .Where(n => n.Email == "test@test.com")
                          .Single();
        return customer;
    }
    private void SaveCustomer(TestModel ctx, Customer customer)
    {
        //save back
        ctx.Customers.Attach(customer); // getting error here
        ctx.Entry(customer).State = EntityState.Modified;
        ctx.SaveChanges();
    }
}

在 sql 服务器数据库中,我有 2 个表:

  • Customer - IdNameShippingCountryId(外键)、BillingCountryId(外键)
  • Country - IdName

当我调用ChangeName方法时,出现以下错误:

附加类型为"TestConsoleApp.customermodel.Country"的实体 失败,因为相同类型的另一个实体已具有相同的 主键值。使用"附加"方法或 将实体的状态设置为"未更改"或"已修改"(如果有) 图形中的实体具有冲突的键值。这可能是因为 某些实体是新的,尚未收到数据库生成的密钥 值。在这种情况下,请使用"添加"方法或"已添加"实体状态 跟踪图形,然后将非新实体的状态设置为 "未更改"或"修改"(视情况而定)。

我一直在做一些调试,发现以下内容:

  • 如果我删除AsNoTracking调用,则没有错误
  • 如果我删除其中一个包含(保留AsNoTracking),则没有错误

因此,看起来相同类型的 AsNoTracking 和 2 包含的组合会导致问题。

任何人都可以建议我为什么收到此错误以及如何解决它?(同时在我的代码中保留AsNoTracking和 2 包含)。

我在其他地方重用了 GetCustomer 方法,这就是为什么我想保留AsNoTracking和 2 包含。

具有多个相同类型的 Include 语句的奇怪实体框架错误

它与引用导航属性和AsNotracking有关。

线...

ctx.Entry(customer).State = EntityState.Modified;

。将customer标记为Modified,但也将两国都附加到上下文中作为Unchanged。在这种情况下,这两个国家是相同的。除了,他们不是...

通常,当 EF 使用 Include 具体化对象时,EF 在对象图中只创建每个实体的一个实例。上下文缓存是一个身份映射:每个实体只出现一次。因此,当两个国家/地区相同时,将在缓存中找到一个Country实体。

但是,对于AsNoTracking,EF 不会将任何内容提交到其缓存中。现在创建了同一国家/地区的两个实例。因此,附加它们时例外。

你不需要这个方法。

private void SaveCustomer(TestModel ctx, Customer customer)
{
    //save back
    ctx.Customers.Attach(customer); //GET ERROR HERE
    ctx.Entry(customer).State = EntityState.Modified;
    ctx.SaveChanges();
}

只是做

    using (TestModel ctx = new TestModel())
    {
        var customer = GetCustomer(ctx);
        //set new name
        customer.Name = "New Name";
        ctx.SaveChanges();
    }

基本上,您检索到的客户不是一个孤立的实体。 它来自 CTX,并且已由 EF 跟踪。 您无需再次附加它。

仅当在代码中重新创建了实体,但手动使用主键时,才附加实体。

例如

// where 1023 is the key of an existing entity in the DB
var customer = new Customer { Id = 1023, Name = "test" };
ctx.Customers.Attach(customer)

您的代码并非如此。 您的实体来自通过 CTX 的 EF 查询,并且已由 EF 跟踪。

问题与多个包含无关。

所以看起来AsNoTracking和2的组合包括 属于导致问题的同一类型。

这就是问题所在。 当您将客户附加到:

ctx.Customers.Attach(customer);

它试图两次附加同一个国家。 发货国家/地区和帐单国家/地区相同,因为它们具有相同的密钥。

从 https://msdn.microsoft.com/library/bb896271(v=vs.100).aspx:

如果要附加的对象具有相关对象,则这些对象也会附加到对象上下文。

所以客户是附加的,然后.Country是附加的,然后是.Country1附加的。 CountryCountry1 是相同的,因此例外,因为同样来自 https://msdn.microsoft.com/library/bb896271(v=vs.100).aspx,当您附加实体时:

如果特定类型的多个实体具有相同的键值,则实体框架将引发异常。

要解决此问题,请进入SaveCustomer并将Attach更改为 Add ,如下所示:

private void SaveCustomer(TestModel ctx, Customer customer)
{
    //save back
    ctx.Customers.Add(customer); // no more error!
    ctx.Entry(customer).State = EntityState.Modified;
    // Prevent countries from being added to database (they already exist in db)
    ctx.Entry(customer.Country).State = EntityState.Detatched;
    ctx.Entry(customer.Country1).State = EntityState.Detatched;
    ctx.SaveChanges();
}

是的,您正在调用 Add ,它通常用于添加数据库中尚不存在的新数据,这将导致向数据库插入。 但是,由于我们要进入并手动将EntityState设置为 Modified ,因此将有一个 UPDATE 语句而不是发送到数据库的 INSERT 语句。

我编写了一个带有一些单元测试的小型测试应用程序来更全面地演示这一点,可以在 https://github.com/codethug/EF.Experiments/blob/master/EF.Test/AttachingMultiple.cs