【问题标题】:Entity Framework 6 - use my getHashCode()实体框架 6 - 使用我的 getHashCode()
【发布时间】:2014-01-28 17:01:01
【问题描述】:

这篇文章有一定的背景知识 - 请耐心等待!

我们有一个使用 EF 的 n 层 WPF 应用程序 - 我们通过 dbContext 将数据库中的数据加载到 POCO 类中。 dbContext 被销毁,然后用户可以编辑数据。 我们使用 Julie Lerman 在她的“编程实体框架:DBContext”一书中建议的“状态绘制”,这样当我们将根实体添加到新的 dbContext 进行保存时,我们可以设置每个子实体是否被添加、修改或保持不变等.

我们第一次这样做时遇到的问题(早在 2012 年 11 月!)是,如果我们添加到 dbContext 的根实体具有相同子实体的多个实例(即,“任务”记录链接到用户,“状态历史记录”也链接到同一用户)该过程将失败,因为即使子实体相同(来自同一数据库行),它们也被赋予了不同的哈希码,因此 EF 将它们识别为不同的对象。

我们解决了这个问题(早在 2012 年 12 月!),方法是在我们的实体上覆盖 GetHashCode,如果实体来自数据库,则返回数据库 ID,如果实体尚未保存,则返回唯一的负数。现在,当我们将根实体添加到 dbContext 时,它很聪明地意识到同一个子实体被多次添加并且它正确地处理了它。自 2012 年 12 月以来一直运行良好,直到我们上周升级到 EF6...

EF6 的新“功能”之一是它现在使用自己的 Equals 和 GetHashCode 方法来执行更改跟踪任务,而忽略任何自定义覆盖。请参阅:http://msdn.microsoft.com/en-us/magazine/dn532202.aspx(搜索“减少对您的编码风格的干扰”)。如果您希望 EF 管理更改跟踪,这很好,但在断开连接的 n 层应用程序中我们不希望这样做,事实上这会破坏我们已经运行良好一年多的代码。

希望这是有道理的。

现在 - 问题 - 有没有人知道我们可以告诉 EF6 使用 OUR GetHashCode 和 Equals 方法,就像它在 EF5 中所做的那样,或者有没有人有更好的方法来处理添加将根实体复制到其中包含重复子实体的 dbContext 以便 EF6 满意?

感谢您的帮助。抱歉,帖子太长了。

更新 在 EF 代码中查看后,看起来 InternalEntityEntry (dbEntityEntry) 的哈希码过去是通过获取实体的哈希码来设置的,但现在在 EF6 中是通过使用 RuntimeHelpers.GetHashCode(_entity) 来检索的,这意味着我们被覆盖的哈希码在实体上被忽略。所以我猜想让 EF6 使用我们的哈希码是不可能的,所以也许我需要专注于如何在不扰乱 EF 的情况下将实体添加到可能具有重复子实体的上下文中。有什么建议吗?

更新 2 最烦人的是,这种功能上的变化被报告为一件好事,而不是在我看来,这是一个重大变化!当然,如果您断开了实体,并且您已经使用 .AsNoTracking() 加载它们以提高性能(并且因为我们知道我们将断开它们,所以为什么要跟踪它们)那么 dbContext 没有理由覆盖我们的 getHashcode 方法!

更新 3 感谢所有 cmets 和建议 - 非常感谢! 经过一些实验,它似乎确实与 .AsNoTracking() 有关。如果您使用 .AsNoTracking() 加载数据,则重复的子实体是内存中的单独对象(具有不同的哈希码),因此会出现问题状态绘制并稍后保存它们。我们之前通过覆盖哈希码解决了这个问题,所以当实体被添加回保存上下文时,重复的实体被识别为同一个对象并且只添加一次,但我们不能再使用 EF6 执行此操作。所以现在我需要进一步调查为什么我们首先使用 .AsNoTracking() 。 我的另一个想法是,也许 EF6 的更改跟踪器应该只对它正在主动跟踪的条目使用它自己的哈希码生成方法 - 如果实体已经加载了 .AsNoTracking() 也许它应该改用来自底层实体的哈希码?

更新 4 所以现在我们已经确定我们不能在 EF6 中继续使用我们的方法(覆盖哈希码和 .AsNoTracking),我们应该如何管理断开连接的实体的更新?我用 blogposts/cmets/authors 创建了这个简单的例子:

在这个示例中,我想打开博文 1,更改内容和作者,然后再次保存。我用 EF6 尝试了 3 种方法,但我无法让它工作:

BlogPost blogpost;

using (TestEntities te = new TestEntities())
{
    te.Configuration.ProxyCreationEnabled = false;
    te.Configuration.LazyLoadingEnabled = false;

    //retrieve blog post 1, with all comments and authors
    //(so we can display the entire record on the UI while we are disconnected)
    blogpost = te.BlogPosts
        .Include(i => i.Comments.Select(j => j.Author)) 
        .SingleOrDefault(i => i.ID == 1);
}

//change the content
blogpost.Content = "New content " + DateTime.Now.ToString("HH:mm:ss");

//also want to change the author from Fred (2) to John (1)

//attempt 1 - try changing ID? - doesn't work (change is ignored)
//blogpost.AuthorID = 1;

//attempt 2 - try loading the author from the database? - doesn't work (Multiplicity constraint violated error on Author)
//using (TestEntities te = new TestEntities())
//{
//    te.Configuration.ProxyCreationEnabled = false;
//    te.Configuration.LazyLoadingEnabled = false;
//    blogpost.AuthorID = 1;
//    blogpost.Author = te.Authors.SingleOrDefault(i => i.ID == 1);
//}

//attempt 3 - try selecting the author already linked to the blogpost comment? - doesn't work (key values conflict during state painting)
//blogpost.Author = blogpost.Comments.First(i => i.AuthorID == 1).Author;
//blogpost.AuthorID = 1;


//attempt to save
using (TestEntities te = new TestEntities())
{
    te.Configuration.ProxyCreationEnabled = false;
    te.Configuration.LazyLoadingEnabled = false;
    te.Set<BlogPost>().Add(blogpost); // <-- (2) multiplicity error thrown here

    //paint the state ("unchanged" for everything except the blogpost which should be "modified")
    foreach (var entry in te.ChangeTracker.Entries())
    {
        if (entry.Entity is BlogPost)
            entry.State = EntityState.Modified;
        else
            entry.State = EntityState.Unchanged;  // <-- (3) key conflict error thrown here
    }

    //finished state painting, save changes
    te.SaveChanges();

}

如果您在 EF5 中使用此代码,请使用我们现有的将 .AsNoTracking() 添加到原始查询的方法..

        blogpost = te.BlogPosts
        .AsNoTracking()
        .Include(i => i.Comments.Select(j => j.Author)) 
        .SingleOrDefault(i => i.ID == 1);

..并覆盖实体上的 GetHashCode 和 Equals:(例如,在 BlogPost 实体中)..

    public override int GetHashCode()
    {
        return this.ID;
    }

    public override bool Equals(object obj)
    {
        BlogPost tmp = obj as BlogPost;
        if (tmp == null) return false;
        return this.GetHashCode() == tmp.GetHashCode();
    }

..代码中的所有三种方法现在都可以正常工作了。

请您告诉我如何在 EF6 中实现这一点?谢谢

【问题讨论】:

  • 您能否在数据上下文对象的扩展方法中覆盖它,或者修改代码生成模板以包含您的覆盖?
  • 会很难,如果可能的话。我认为您将不得不在 ApplyChanges 方法期间自己跟踪哈希码:如果它的哈希码已经通过,则跳过一个对象。
  • LordTakkera - 我们更改了标准代码生成模板以覆盖我们所有实体上的 GetHashCode 和 Equals。 Gert Arnold - 实体拥有我们的哈希码,直到我们将它们添加到 dbContext。但是,以前 EF 会使用我们的哈希码来确定对象图中的两个实体是否相同并且只添加一次,但现在它会忽略哈希码并使用两个不同的哈希码添加两次“相同”实体。我可能会尝试过滤对象图中的实体,以确保仅将不同的对象添加到 dbContext。我会回来报告的..
  • 这篇文章的答案可能会让你感兴趣:stackoverflow.com/q/21421008/861716
  • 你不应该有某种身份映射和每个对象一个(且只有一个)实例吗?

标签: c# wpf entity-framework dbcontext n-tier-architecture


【解决方案1】:

让您的应用程序在 EF5 中以这种方式工作,这既有趣又令人惊讶。 EF 始终只需要任何实体的单个实例。如果添加了一个对象图,并且 EF 错误地假定它已经在跟踪一个对象,而实际上它正在跟踪一个不同的实例,那么 EF 正在跟踪的内部状态将是不一致的。例如,该图仅使用 .NET 引用和集合,因此该图仍会有多个实例,但 EF 只会跟踪一个实例。这意味着可能无法正确检测到实体属性的更改,并且实例之间的修复也可能导致意外行为。想知道您的代码是否以某种方式解决了这些问题,或者只是碰巧您的应用没有遇到任何这些问题,因此无效状态跟踪对您的应用无关紧要,这会很有趣。

我们对 EF6 所做的更改使应用不太可能将 EF 状态跟踪变为无效状态,从而导致意外行为。如果你有一个聪明的模式来确保跟踪状态是有效的,我们打破了 EF6,那么如果你可以在 http://entityframework.codeplex.com/ 提交一个带有完整重现的错误,那就太好了。

【讨论】:

  • 感谢您的回答 - 我们相信,当我们使用 .Add() 方法添加实体时,EF 会自动处理具有相同哈希码的两个实体,但从您的回答中听起来它实际上是忽略并且不跟踪第二个中的更改。我认为我们的方法(通过使用数据库 ID 覆盖哈希码)仍然对我们有用,因为当对象图中同一实体的多个实例时,它们通常是“查找”类型的项目,因此之前状态被绘制为“未更改”保存...
  • ..我的问题 - 在 EF6 中更新和保存断开连接的实体的最佳做法是什么?我们的方法在 EF5 中对我们有效,但现在在 EF6 中却不行。请参阅上面 UPDATE 4 中的一些示例代码。再次感谢。
  • @Buzby 这篇文章是前段时间由 EF 团队的 Diego 写的:blogs.msdn.com/b/diego/archive/2010/10/06/…。它谈到了自我跟踪实体,但一般方法也适用于这种情况。我认为这篇文章中最重要的一点是尽可能避免创建重复项。如果这是不可能的,那么帖子会展示一些解决重复问题的方法,但这通常是一个很难解决的问题。
猜你喜欢
  • 2015-12-26
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-11-21
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多