EF6 无法将分离的实体附加到上下文

Posted

技术标签:

【中文标题】EF6 无法将分离的实体附加到上下文【英文标题】:EF6 Could not attach a detached entity to context 【发布时间】:2021-11-26 12:03:12 【问题描述】:

我有一个问题,我无法将实体附加到 DBContext,尽管它显示其 Enity.State 为已分离。

我使用 DBContext 获取实体,然后将其处理掉,然后进行一些更改并尝试使用新的 DBContext 保存这些新更改,因此显然新的 DBContext 没有将这些实体视为附加,因此我需要将它们附在上面。

当我尝试使用下面的代码附加实体时

db.Receipts.Attach(receipt);

我得到了那个错误

“ObjectStateManager中已经存在同一个key的对象,ObjectStateManager无法跟踪多个同一个key的对象”

我尝试在第一次获取 DBContext 时使用 AsNoTracking() 获取实体,但仍然显示相同的错误。

我尝试使用db.Entry(receipt).State = System.Data.Entity.EntityState.Modified;进行附加

也尝试使用objectContext.ObjectStateManager.ChangeObjectState(receipt, System.Data.Entity.EntityState.Modified);

仍然有同样的问题。

我想提一下,我使用的是旧的 .Net Framework 4.0 和 Enity Framework 6.0。

任何帮助将不胜感激。

【问题讨论】:

【参考方案1】:

您可以执行以下操作来获取对新 DBContext 的更改:

            System.Data.Entity.Core.Objects.ObjectContext objectContext = ((IObjectContextAdapter)db).ObjectContext;
            string entitySetName = objectContext.CreateObjectSet<Receipts>().EntitySet.Name;
            objectContext.ApplyCurrentValues<Receipts>(entitySetName, receipt);

编辑: 您必须先从数据库加载原始实体(如果尚未加载):

IDbSet<Receipts> dbset = db.Set<Receipts>();
var dbReceipt = dbset.Find(id)

【讨论】:

【参考方案2】:

在循环处理行和使用相关实体时通常会出现此错误,尤其是在这些实体已被序列化/反序列化或作为修改过程的一部分克隆的情况下。

假设我从 DbContext 加载引用同一团队的玩家列表。

 Player (Id: 1, Name: George, TeamId: 10) [REF 101]
 Player (Id: 2, Name: Simon, TeamId: 10) [REF 102]
 Team (Id: 10, Name: Jazz) [REF 103]

George 和 Simon 都会急切地返回他们的团队,这两个团队都将引用单个团队实体 [REF 103]。

如果我们分离这些,然后打开一个新的 DbContext 并重新附加:

context.Players.Attach(player1);
context.Teams.Attach(player1.Team);
context.Players.Attach(player2);
context.Teams.Attach(player2.Team);

这应该可以按预期工作。即使我们两次附加同一个团队,它也是同一个引用,并且 EF 将跳过第二次调用,因为该引用已附加。警告:自从我使用 EF4 以来已经有很长时间了,所以这种行为可能会有所不同,并已通过 EF6 进行了验证。您可能需要在附加之前检查 DbContext 以查看实体是否已分离:

if(context.Entry(player1).State == EntityState.Detached)
    context.Players.Attach(player1);
if(context.Entry(player1.Team).State == EntityState.Detached)
    context.Teams.Attach(player1.Team);
if(context.Entry(player2).State == EntityState.Detached)
    context.Players.Attach(player2);
if(context.Entry(player2.Team).State == EntityState.Detached)
    context.Teams.Attach(player2.Team);

现在,如果这些实体通过序列化程序并返回(例如通过 ASP.Net 表单提交或 Ajax 调用等完成),我们得到的是具有新引用的新断开连接的实体,但有一个关键区别:

Player (Id: 1, Name: George, TeamId: 10) [REF 201]
Player (Id: 2, Name: Simon, TeamId: 10) [REF 202]
Team (Id: 10, Name: Jazz) [REF 203]
Team (Id: 10, Name: Jazz) [REF 204]

George 将引用一个团队,在本例中为 REF 203,而 Simon 将引用同一团队的一个实例,但 REF 204。2 个实例包含相同的数据。

当我们将 Player 1 [REF 201]/w Team 10 [REF 203] 附加到新的 DbContext 时,一切都按预期进行。但是,当我们附加玩家 2 时,我们会收到玩家 2 的团队参考的错误:

context.Players.Attach(player1);
context.Teams.Attach(player1.Team);
context.Players.Attach(player2);
context.Teams.Attach(player2.Team); // <-- Boom

DbContext 将根据玩家 1 的参考来跟踪 Team #10。即使检查附加/分离状态也无济于事,因为它们是不同的引用。

为了解决这个问题,我们需要在附加之前始终检查 DbContext 中是否存在已跟踪的引用。这可以通过查询 DbSet 的 Local 属性来完成。这不会命中数据库,它只会检查本地跟踪的引用。处理分离实体的安全方法:

var trackedTeam = context.Teams.Local.SingleOrDefault(x => x.Id == player1.Team.Id);
if (trackedTeam == null)
    context.Teams.Attach(player1.Team);
else
    player1.Team = trackedTeam;

var trackedPlayer = context.Players.Local.SingleOrDefault(x => x.Id == player1.Id);
if (trackedPlayer == null)
    context.Players.Attach(player1);


trackedTeam = context.Teams.Local.SingleOrDefault(x => x.Id == player2.Team.Id);
if (trackedTeam == null)
    context.Teams.Attach(player2.Team);
else
    player2.Team = trackedTeam;

trackedPlayer = context.Players.Local.SingleOrDefault(x => x.Id == player2.Id);
if (trackedPlayer == null)
    context.Players.Attach(player2);

在 DbContext 已经在跟踪实体的情况下,您可能需要额外的代码来比较未跟踪副本的值并将其复制到已跟踪的现有实例。使用引用安全地处理分离的实体可能是相当笨拙的过程。

【讨论】:

以上是关于EF6 无法将分离的实体附加到上下文的主要内容,如果未能解决你的问题,请参考以下文章

EF6基础系列(十)---离线场景保存实体和实体图集

将实体附加到数据上下文

将 Linq 克隆到 Sql 实体 - 分离数据上下文

EF6基础系列(12)--- EF进行批量添加/删除

EF6 模型优先 - 实体类型不是当前上下文模型的一部分

如何正确地将已附加到上下文的实体标记为已修改?