尝试通过属性默认值更改关系时发生意外的InvalidOperationException


10

在下面的示例代码中,执行此操作时出现以下异常db.Entry(a).Collection(x => x.S).IsModified = true

System.InvalidOperationException:'无法跟踪实体类型'B'的实例,因为已经跟踪了具有键值'{Id:0}'的另一个实例。附加现有实体时,请确保仅附加一个具有给定键值的实体实例。

为什么不添加而不是附加B的实例?

奇怪的是,文档IsModified未指定InvalidOperationException可能的例外。无效的文档或错误?

我知道这段代码很奇怪,但是我写它只是为了了解ef core在某些奇怪的egde情况下是如何工作的。我想要的是一个解释,而不是变通的方法。

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;

class Program
{
    public class A
    {
        public int Id { get; set; }
        public ICollection<B> S { get; set; } = new List<B>() { new B {}, new B {} };
    }

    public class B
    {
        public int Id { get; set; }
    }

    public class Db : DbContext {
        private const string connectionString = @"Server=(localdb)\mssqllocaldb;Database=Apa;Trusted_Connection=True";

        protected override void OnConfiguring(DbContextOptionsBuilder o)
        {
            o.UseSqlServer(connectionString);
            o.EnableSensitiveDataLogging();
        }

        protected override void OnModelCreating(ModelBuilder m)
        {
            m.Entity<A>();
            m.Entity<B>();
        }
    }

    static void Main(string[] args)
    {
        using (var db = new Db()) {
            db.Database.EnsureDeleted();
            db.Database.EnsureCreated();

            db.Add(new A { });
            db.SaveChanges();
        }

        using (var db = new Db()) {
            var a = db.Set<A>().Single();
            db.Entry(a).Collection(x => x.S).IsModified = true;
            db.SaveChanges();
        }
    }
}

A和B有什么关系?含义是什么关系属性?
山姆

Answers:


8

提供的代码中的错误原因如下。

当您A从数据库中创建实体时,将S使用包含两个新记录的集合来初始化其属性BId每个新B实体的等于0

// This line of code reads entity from the database
// and creates new instance of object A from it.
var a = db.Set<A>().Single();

// When new entity A is created its field S initialized
// by a collection that contains two new instances of entity B.
// Property Id of each of these two B entities is equal to 0.
public ICollection<B> S { get; set; } = new List<B>() { new B {}, new B {} };

执行代码行后,实体var a = db.Set<A>().Single()集合不包含来自数据库的实体,因为它不使用延迟加载并且没有显式加载集合。实体仅包含在初始化集合期间创建的新实体。SABDbContext DbSABS

当您要求IsModifed = true收集时,S实体框架会尝试将这两个新实体添加B到变更跟踪中。但这失败了,因为两个新B实体具有相同的属性Id = 0

// This line tries to add to change tracking two new B entities with the same Id = 0.
// As a result it fails.
db.Entry(a).Collection(x => x.S).IsModified = true;

您可以从堆栈跟踪中看到,实体框架尝试将B实体添加到IdentityMap

at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.IdentityMap`1.ThrowIdentityConflict(InternalEntityEntry entry)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.IdentityMap`1.Add(TKey key, InternalEntityEntry entry, Boolean updateDuplicate)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.IdentityMap`1.Add(TKey key, InternalEntityEntry entry)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.IdentityMap`1.Add(InternalEntityEntry entry)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.StartTracking(InternalEntityEntry entry)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry.SetPropertyModified(IProperty property, Boolean changeState, Boolean isModified, Boolean isConceptualNull, Boolean acceptChanges)
at Microsoft.EntityFrameworkCore.ChangeTracking.NavigationEntry.SetFkPropertiesModified(InternalEntityEntry internalEntityEntry, Boolean modified)
at Microsoft.EntityFrameworkCore.ChangeTracking.NavigationEntry.SetFkPropertiesModified(Object relatedEntity, Boolean modified)
at Microsoft.EntityFrameworkCore.ChangeTracking.NavigationEntry.set_IsModified(Boolean value)

错误消息还表明它无法跟踪B实体,Id = 0因为已经跟踪了B具有相同实体的另一个实体Id


如何解决这个问题。

若要解决此问题,您应该删除B在初始化S集合时创建实体的代码:

public ICollection<B> S { get; set; } = new List<B>();

相反,您应该SA创建的地方填充收藏。例如:

db.Add(new A {S = {new B(), new B()}});

如果您不使用延迟加载,则应显式加载Scollection以将其项添加到变更跟踪中:

// Use eager loading, for example.
A a = db.Set<A>().Include(x => x.S).Single();
db.Entry(a).Collection(x => x.S).IsModified = true;

为什么不添加而不是附加B的实例?

简而言之,由于它们具有Detached状态,因此很容易被添加。

执行代码行后

var a = db.Set<A>().Single();

创建的实体实例B具有state Detached。可以使用以下代码进行验证:

Console.WriteLine(db.Entry(a.S[0]).State);
Console.WriteLine(db.Entry(a.S[1]).State);

然后当你设定

db.Entry(a).Collection(x => x.S).IsModified = true;

EF尝试添加B实体以更改跟踪。从EFCore的源代码中,您可以看到这将我们引向方法InternalEntityEntry.SetPropertyModified,并带有下一个参数值:

  • property-我们的B实体之一
  • changeState = true
  • isModified = true
  • isConceptualNull = false
  • acceptChanges = true

具有此类参数的此方法将Detached B实体的状态更改为Modified,然后尝试开始对其进行跟踪(请参见第490-506行)。因为B实体现在具有状态,Modified这导致它们被附加(未添加)。


“为什么不添加而不是附加B的实例?”的答案在哪里?您说的是“失败,因为两个新的B实体都具有相同的ID = 0”。我认为这是错误的,因为ef核心会同时保存1和2个ID。我认为这不是正确的答案
DIlshod K

@DIlshod K感谢您的评论。在“如何解决此问题”部分中,我写道,S应显式加载集合,因为提供的代码不使用延迟加载。当然,EF将先前创建的B实体保存在数据库中。但是代码行A a = db.Set<A>().Single()仅加载A集合中没有实体的实体S。要进行负载收集,S应使用急切的负载。我将更改答案,以明确包括以下问题的答案:“为什么不添加而不是附加B的实例?”。
Iliar Turdushev
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.