每组最高:Take(1)有效,但FirstOrDefault()没有?

时间:2012-08-27 08:10:37

标签: c# .net entity-framework entity-framework-4.1 dbcontext

我正在使用EF 4.3.1 ...刚刚升级到4.4(问题仍然存在),其中包含 EF 4.x DbContext Generator 。我有以下数据库名为'Wiki'(用于创建表和数据的SQL脚本是here):

Author(ID, Name) <-- Article(AuthorID, Title, Revision, CreatedUTC, Body)

编辑wiki文章时,新版本将作为新记录插入而不是更新其记录,并且修订计数器会递增。在我的数据库中有一位作者“John Doe”,它有两篇文章,“文章A”和“文章B”,其中文章A有两个版本(1和2),但文章B只有一个版本。

enter image description here

我同时禁用了延迟加载和代理创建(here是我在LINQPad中使用的示例解决方案)。我想获得由名字以“John”开头的人创建的文章的最新版本,所以我做了以下查询:

Authors.Where(au => au.Name.StartsWith("John"))
       .Select(au => au.Articles.GroupBy(ar => ar.Title)
                                .Select(g => g.OrderByDescending(ar => ar.Revision)
                                              .FirstOrDefault()))

这会产生错误的结果,并只检索第一篇文章:

enter image description here

通过将.FirstOrDefault()替换为.Take(1),对查询进行少量更改会导致以下查询:

Authors.Where(au => au.Name.StartsWith("John"))
       .Select(au => au.Articles.GroupBy(ar => ar.Title)
                                .Select(g => g.OrderByDescending(ar => ar.Revision)
                                              .Take(1)))

令人惊讶的是,此查询产生了正确的结果(尽管有更多嵌套):

enter image description here

我假设EF生成稍微不同的SQL查询,一个只返回单个文章的最新版本,另一个返回所有文章的最新版本。两个查询生成的丑陋SQL差别不大(比较:SQL for .FirstOrDefault() vs SQL for .Take(1)),但它们都返回正确的结果:

.FirstOrDefault()

enter image description here

.Take(1)(重新排列列顺序以便于比较)

enter image description here

因此,罪魁祸首不是生成的SQL,而是EF对结果的解释。为什么EF将第一个结果解释为单个Article实例,而它将第二个结果解释为两个Article个实例?为什么第一个查询返回不正确的结果?

编辑:我在Connect上打开了bug report。如果您认为解决此问题很重要,请提前注册。

4 个答案:

答案 0 :(得分:3)

观察:
http://msdn.microsoft.com/en-us/library/system.linq.enumerable.firstordefault
http://msdn.microsoft.com/en-us/library/bb503062.aspx
关于Take是如何工作(懒惰,早期brekaing)的解释非常好,但FirstOrDefault都没有。更重要的是,看到了Take的解释,我得到了猜测&#39;由于尝试在SQL中模拟延迟评估,使用Take的查询可能会减少行数,而您的情况则表明它是另一种方式!我确实理解为什么你会观察到这种效果。

它可能只是特定于实现的..对我来说,Take(1)和FirstOrDefault可能看起来都像TOP 1,但从功能的角度来看,它们的&可能会略有不同。 #39;懒惰&#39;:一个函数可以评估所有元素并返回第一个,第二个可以先评估然后返回它并打破评估。它只是一个&#34;提示&#34;可能发生的事情。对我来说,这是一个废话,因为我没有看到关于这个主题的文档,一般来说我确定Take / FirstOrDefault都是懒惰的,并且应该只评估前N个元素。

在查询的第一部分中,group.Select + orderBy + TOP1是&#34;清除指示&#34;你感兴趣的是具有最高价值的单行&#39;在每个组的一列中 - 但事实上,有no simple way to do declare that in SQL,所以对于SQL引擎和EF引擎都没有明确的指示。

至于我,你提出的行为可能表明FirstOrDefault已经传播了#39;由EF翻译器向上提升了一层内部查询,就好像是在Article.GroupBy()中(你确定你没有错误地将parens放在OrderBy上吗?:)) - 这将是一个错误。

但是 -

由于差异必须在执行的意义和/或顺序中,所以让我们看看EF可以猜测查询的含义。作者实体如何获得其文章? EF如何知道哪个文章与您的作者绑定?当然,nav属性。但是如果只有一些文章被预加载呢?看起来很简单 - 查询返回一些带有列的结果,列描述整个作者和整篇文章,因此我们将它们映射到作者和文章,并让它们相互匹配导航键。好。但是将复杂的过滤添加到那个......?

使用简单的过滤器(如按日期),对于所有文章,它是单个子查询,按日期截断行,并且消耗所有行。但是如何编写一个复杂的查询,使用多个中间排序并生成多个文章子集?应该将哪个子集绑定到生成的作者?所有人的联盟?这将使所有顶级类似条款无效。第一个?废话,第一个子查询往往是中间帮助者。因此,可能的情况是,当查询被视为一组具有相似结构的子查询时,所有子查询都可以作为部分加载nav属性的数据源,那么很可能只将最后一个子查询作为实际结果。这都是抽象思维,但它让我注意到Take()与FirstOrDefault及其整体Join与LeftJoin意义实际上可能会改变结果集扫描的顺序,并且,不知何故,Take()以某种方式进行了优化并在一次扫描中完成整个结果,因此一次访问所有作者的文章,并且FirstOrDefault作为直接扫描for each author * for each title-group * select top one and check count and substitue for null执行,其中多次为每个作者生成小的单项文章集合,从而导致一个结果 - 仅来自最后一次访问的标题组。

这是我能想到的唯一解释,除了明显的&#34; BUG!&#34;喊。作为LINQ用户,对我来说,它仍然是一个bug。这样的优化根本不应该发生,或者它也应该包括FirstOrDef - 因为它与Take(1).DefaultIfEmpty()相同。嘿,顺便说一下 - 你试过吗?正如我所说,由于JOIN / LEFTJOIN含义,Take(1)与FirstOrDefault不同 - 但Take(1).DefaultIfEmpty()实际上在语义上是相同的。查看它在SQL上生成的SQL查询以及EF层的结果会很有趣。

我不得不承认,部分加载中的相关实体的选择对我来说从来都不清楚,而且我实际上没有使用部分加载一段时间,因为我总是说查询,以便结果和分组明确定义(*)..因此,我可能只是忘记了其内部工作的一些关键方面/规则/定义,也许,即。它实际上是从结果集中选择每个相关记录(而不仅仅是我现在描述的最后一个子集合)。如果我忘记了什么,我刚才描述的一切都显然是错误的。

(*)在您的情况下,我也将Article.AuthorID设为导航属性(公共作者设置),然后重写类似于更平坦/流水线的查询,如:

var aths = db.Articles
              .GroupBy(ar => new {ar.Author, ar.Title})
              .Take(10)
              .Select(grp => new {grp.Key.Author, Arts = grp.OrderByDescending(ar => ar.Revision).Take(1)} )

然后分别用一对作者和艺术填充视图,而不是试图部分填充作者并仅使用作者。顺便说一句。我没有针对EF和SServer对其进行测试,这只是一个将查询翻转过来的示例&#39;并且&#39;展平&#39;在JOIN的情况下子查询并且对于LEFTJOIN是不可用的,所以如果您还想查看没有文章的作者,它必须从作者开始,就像您的原始查询一样。

我希望这些松散的想法能帮助我找到原因&#39; ..

答案 1 :(得分:2)

FirstOrDefault()方法是即时的,而另一个方法(Take(int))推迟到执行。

答案 2 :(得分:0)

正如在前面的回答中我试图解释这个问题 - 我辞职了,我正在写另一个:)再看一遍后,我认为这是一个错误。我认为您应该使用Take并将案例发布到Microsoft的Connect,并检查他们对此有何看法。

以下是我找到的内容:http://connect.microsoft.com/VisualStudio/feedback/details/658392/linq-to-entities-orderby-is-lost-when-followed-by-firstordefault

“Microsoft 2011-09-22 at 16:07”的回复详细描述了EF内部的一些优化机制。在一些地方,他们说关于重新排序skip / take / orderby,有时逻辑不会识别某些结构。我认为你刚刚偶然发现了另一个在'orderby lifting'中没有正确分支的角落情况。总而言之,在生成的SQL中,你在order-by中有select-top-1,而损坏看起来就像将'top 1'提升到一个太高的水平!

答案 3 :(得分:0)

今天,我刚刚发现,如果q.OrderBy(a=>a.Customer.FirstOrDefault().Name)位于排序子句中,则FirstOrDefault()将不会对数据库产生影响。