附加从多个源检索到的对象

本文关键字:检索 对象 | 更新日期: 2023-09-27 18:10:45

我有三个类,Fish(它分别包含两个类型为Chips和MushyPeas的属性)、MushyPeas(它包含一个类型为Chips的属性)和Chips(它有一个Name属性)。

我正在运行下面一段假设代码:

int chipsId;
using (var db = new FishContext())
{
    var creationChips = new Chips() { Name = "A portion of chips" };
    db.Chips.Add(creationChips);
    db.SaveChanges();
    chipsId = creationChips.ChipsId;
}
Chips retrievedChips1;
using (var db = new FishContext())
{
    retrievedChips1 = db.Chips.Where(x => x.ChipsId == chipsId).ToList()[0];
}
Chips retrievedChips2;
using (var db = new FishContext())
{
    retrievedChips2 = db.Chips.Where(x => x.ChipsId == chipsId).ToList()[0];
}
using (var db = new FishContext())
{
    db.Chips.Attach(retrievedChips1);
    db.Chips.Attach(retrievedChips2);
    var mushyPeas = new MushyPeas() { Chips = retrievedChips2 };
    var fish = new Fish() { Chips = retrievedChips1, MushyPeas = mushyPeas };
    db.Fish.Add(fish);
    db.ChangeTracker.DetectChanges();
    db.SaveChanges();
}

这是为了模拟我的真实应用程序中的一种情况,其中EF对象(实际上可能代表相同的数据库记录)从各种不同的DbContext中加载,然后添加到另一个DbContext中的对象树。

如果我不调用两个db.Chips.Attach行,那么当Fish对象保存到数据库时,将创建全新的Chips实体,并分配新的id。

调用db.Chips.Attach解决了其中一个检索对象的这个问题,但是第二个Attach调用失败,并出现异常"具有相同键的对象已经存在于ObjectStateManager中。ObjectStateManager不能跟踪具有相同键的多个对象。"

实现我在这里想要实现的目标的最好方法是什么?

附加从多个源检索到的对象

作为一名经验丰富的EF兽医,我得出的结论是,在许多情况下最好避免使用Attach

异常"An object with the same key already exists in the ObjectStateManager. The ObjectStateManager cannot track multiple objects with the same key"通常具有误导性,因为您试图附加的对象实际上并没有附加到数据上下文。当您附加一个对象时,它会递归地附加它所引用的任何实体。因此,如果您将一个实体附加到数据上下文,然后附加另一个实体,该实体引用之前隐式附加的任何实体,则会得到此错误。解决方案非常简单:

using (var db = new FishContext())
{
    var chips1 = db.Chips.Find(retrievedChips1.Id);
    var chips2 = db.Chips.Find(retrievedChips2.Id);
    var mushyPeas = new MushyPeas() { Chips = chips2 };
    var fish = new Fish() { Chips = chips1, MushyPeas = mushyPeas };
    db.Fish.Add(fish);
    db.ChangeTracker.DetectChanges();
    db.SaveChanges();
}

这保证了两个实体都将被附加到数据上下文中,而不会出现任何ObjectStateManager问题。

您可以查询Local集合以检查是否已经附加了具有相同键的实体,如果是,则使用附加的实体:

using (var db = new FishContext())
{
    var attachedChips1 = db.Chips.Local
        .SingleOrDefault(c => c.ChipsId == retrievedChips1.ChipsId);
    if (attachedChips1 == null)
    {
        db.Chips.Attach(retrievedChips1);
        attachedChips1 = retrievedChips1;
    }
    var attachedChips2 = db.Chips.Local
        .SingleOrDefault(c => c.ChipsId == retrievedChips2.ChipsId);
    if (attachedChips2 == null)
    {
        db.Chips.Attach(retrievedChips2);
        attachedChips2 = retrievedChips2;
    }
    var mushyPeas = new MushyPeas() { Chips = attachedChips2 };
    var fish = new Fish() { Chips = attachedChips1, MushyPeas = mushyPeas };
    //...
}

(第一个检查在这个简单的例子中没有意义,因为新上下文是空的,没有附加任何内容。但是你懂的…)

但是,如果您还想更新相关实体(例如,在附加后将状态设置为Modified),如果retrievedChips1retrievedChips2具有不同的属性值(除了键值),则会出现问题。你必须以某种方式决定哪一个是"正确的"。但这是商业逻辑。你只需要把其中一个交给EF,而且只有一个。在您的场景中,使用哪个并不重要,因为您只创建了一个关系,并且EF只关心键值。

旁注:代替...ToList()[0]更自然的方式是...First()(或Single()在这种情况下,因为您正在查询密钥)。