【问题标题】:Reattaching a detached entity throws an exception重新附加分离的实体会引发异常
【发布时间】:2020-01-10 05:53:55
【问题描述】:

我正在为我的项目使用 Entity Framework 6(使用生成的模型和 DbContext)。我在我的程序中使用的架构是数据访问层是从应用程序中抽象出来的。这意味着我的实体将大部分处于分离状态。

似乎重新连接任何以前连接的实体似乎是不可能的(或者我没有正确执行)。

我已经单独测试了一个案例,并且能够缩小问题范围。步骤如下。

  1. 从上下文中获取实体并释放上下文。这会分离实体。

    public async Task<ArchiveEntry> GetEntry(string api, string id)
    {
        // Omitted some code for simplicity
        ArchiveEntry entry;
    
        using (var db = new TwinTailDb())
        {
            entry = await db.ArchiveEntries.Where(a => a.Id == id).SingleOrDefaultAsync();
        }
    
        return entry;
    }
    
  2. 无论是否更改,都保存实体。

    public async Task Save()
    {
        // Omitted some code for simplicity
        using (var db = new TwinTailDb())
        {
            db.ArchiveEntries.AddOrUpdate(a => a.Id, this);
            await db.SaveChangesAsync();
        }
    }
    
  3. 最后,重新附加分离的实体。

    public async Task RevertChanges()
    {
        using (var db = new TwinTailDb())
        {
            if (db.Entry(this).State == EntityState.Detached && Id != 0)
            {
                db.ArchiveEntries.Attach(this);
                //await db.Entry(this).ReloadAsync(); // Commented since this is irrelevant
            }
        }
    }
    
  4. 然后我运行这个函数来使用上面的代码。

    public async Task Test()
    {
        ArchiveEntry entry = await ArchiveService.GetEntry(null, "7");
        await entry.Save();
        await entry.RevertChanges();
    }
    

然后它抛出这个错误:

附加类型为“TwinTail.Entities.ArchiveEntry”的实体失败,因为同一类型的另一个实体已经具有相同的主键值。如果图中的任何实体具有冲突的键值,则在使用“附加”方法或将实体的状态设置为“未更改”或“已修改”时,可能会发生这种情况。这可能是因为某些实体是新实体,尚未收到数据库生成的键值。在这种情况下,使用“添加”方法或“已添加”实体状态来跟踪图形,然后将非新实体的状态设置为“未更改”或“已修改”。

这里有一点很重要。如果我跳过第 2 步,它不会引发异常。

我的猜测是实体被修改并保存在不同的上下文中。如果步骤 2 被跳过,实体保持不变,因此重新附加它不会造成问题(只是猜测)。然而,这个实体已经处于分离状态,所以这应该是无关紧要的。

另外一点,ChangeTracker 在这些步骤中不包含任何内容。此外,如果我对分离的实体执行任何上下文操作,它会抛出一个异常,说明它应该首先附加。我还注意到内部的 _entitywrapper 仍然有对旧上下文的引用。

所以,最后,问题来了。如何正确重新附加实体以及为什么会发生此异常。

我在另一个问题 (How to revert changes on detached entities) 中提出了类似的问题,但我觉得我需要发布一个新问题,因为这更笼统。

【问题讨论】:

  • 如果你想从上下文 A 加载一个实体,然后释放上下文 A,然后使用上下文 B 附加/保存它,然后释放上下文 B,然后将其重新附加到上下文 C 以便从数据库,你做错了。
  • 您需要代理实体吗?如果不这样做,您可以禁用代理,实体将停止携带有关其原始DbContext 的信息。
  • @danludwig 哦等等。 “使用”声明还不够吗?我认为它会在 using 块之后自动安全地处理上下文。如果是,那么我做错了一切哈哈。对于每个数据库操作,我都会创建一个新上下文并在函数结束之前(使用 using)处理它。我想我记得读过一篇文章说应该是这样的。但请赐教。
  • @SteveRuble 好的,我会试试的。

标签: c# .net entity-framework


【解决方案1】:

我在我的程序中使用的架构是数据访问层是从应用程序中抽象出来的。

看起来您正在ArchiveEntry 类本身上实现这些方法。这不是一个抽象的数据访问层,将实体传递给许多像这样的短期上下文会给你带来麻烦。

您应该将该代码放入与实体分开的类中,而不是为实体类提供自己的方法来管理持久性问题,并确保一旦实体附加到上下文,您将继续使用相同的上下文,直到它被处理掉了。释放上下文后,如果您想对实体执行其他操作,则应先从新上下文中检索它,然后再尝试对其执行任何(附加)操作。

【讨论】:

  • 同意。模型应该与数据访问分开。我建议使用类似存储库模式的东西。例如。为将处理所有数据库操作的每个模型创建一个存储库类。
  • EF 已经实现了开箱即用的通用存储库模式。我会推荐一种 CQRS 风格的消息传递模式,其中写操作被抽象为命令,具有单独的命令处理程序方法主体。这将更容易拥有一个作用域 DbContext 实例,该实例可以在特定执行上下文的命令之间共享(例如单个 HTTP Web 请求)。
  • 哦,是的,对不起。这些部分类仍在等待重构(原型设计=p)。我确实有 IServices,其工作方式类似于存储库模式。目前,我的重点是让一切都先正常运行,因为这是我第一次同时使用 EF 和 WPF。我其实是一个游戏开发者=p
  • 我应该在这里发表评论。没有注意到你是直接评论这个问题的人。关于上下文生命周期,我看到文章/帖子(大部分)是这样的:stackoverflow.com/questions/10777630/… 它说应该立即处理上下文。你能告诉我这些“烦恼”吗?
  • 嗯,你现在至少遇到了一个麻烦——在上下文之间洗牌实体会破坏事情。你发布的答案自称是有争议的,我和它的作者有不同的意见。这些事情都不是国际海事组织的真正问题。您应该了解延迟加载的后果,并确保您的 IoC 容器正确处理。
【解决方案2】:

我使用的是 AsNoTracking(),所以这个异常对我来说很奇怪。下面的代码解决了我的代码中的问题。我想我可以在 try 块中使用代码并且只使用 catch 块,但我从未测试过这个概念。

具有 PrimaryKey 的实体基类

public class EntityBase<K>
{
    [NotMapped]
    public virtual K PrimaryKey {get;}
}

更新功能

public static void Update<T,K>(DbContext context, T t) where T:EntityBase<K>
{
    DbSet<T> set = context.Set<T>();
    if(set==null) return;
    if(context.Entry<T>(t).State == EntityState.Detached)
    {
         try
         {
             T attached = set.Attached(t);
             context.Entry<T>(attached).State = EntityState.Modified;
         }catch(Exception ex)
         {
             T found = set.Find(t.PrimaryKey);
             context.Entry(found).CurrentValues.SetValues(t);
         }
     }
     context.SaveChanges();
}

扩展功能

public static void Update<T,K>(this DbContext context, T t) where T:EntityBase<K> => Update<T,K>(context, t);

【讨论】:

  • 你能详细说明你的答案吗?
  • 确实有,发帖后请审核。
  • 下面的代码修复了我的代码中的问题。 是的,但我们不知道问题所在。对我们来说,这只是一段随机的(可能)工作代码,与问题无关。
  • 我的代码是一种解决方法。发出的原因是由于将条目与已关闭的上下文进行比较(使用 (var db = new TwinTailDb()))。所以它不是当前上下文的当前条目。
猜你喜欢
  • 1970-01-01
  • 2023-03-08
  • 2014-06-23
  • 2011-08-14
  • 1970-01-01
  • 2019-11-30
  • 1970-01-01
  • 2015-01-03
  • 1970-01-01
相关资源
最近更新 更多