10

我正在使用 EF 4.3.1 ...刚刚升级到 4.4(问题仍然存在),并使用EF 4.x DbContext Generator生成的数据库优先 POCO 实体。我有以下名为“Wiki”的数据库(用于创建表和数据的 SQL 脚本在此处):

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

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

在此处输入图像描述

我禁用了延迟加载和代理创建(是我与 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()))

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

在此处输入图像描述

通过替换为以下查询中的结果,对.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)))

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

在此处输入图像描述

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

.FirstOrDefault()

在此处输入图像描述

.Take(1)(为了便于比较,重新排列了列顺序)

在此处输入图像描述

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

编辑:我已经打开了关于 Connect的错误报告。如果您认为解决此问题很重要,请点赞。

4

4 回答 4

3

看: http:
//msdn.microsoft.com/en-us/library/system.linq.enumerable.firstordefault
http://msdn.microsoft.com/en-us/library/bb503062.aspx
有很好的解释Take是如何工作的(懒惰的,早期的休息),但没有FirstOrDefault..更重要的是,看到Take的解释,我“猜测”它的查询可能会由于试图模仿懒惰而减少行数SQL中的评估,您的情况表明它是另一种方式!我不明白你为什么要观察这种效果。

它可能只是特定于实现的.. 对我来说, Take(1) 和 FirstOrDefault 可能看起来像TOP 1,但是从功能的角度来看,它们的“惰性”可能略有不同:一个函数可以评估所有元素并首先返回, second 可以先评估然后返回并中断评估。这只是对可能发生的事情的“提示”。对我来说,这是胡说八道,因为我没有看到关于这个主题的文档,而且总的来说,我确信 Take/FirstOrDefault 都是惰性的,应该只评估前 N 个元素。

在查询的第一部分, group.Select+orderBy+TOP1 是一个“明确的指示”,表明您对每组列中具有最高“值”的单行感兴趣-但实际上,没有简单的方法在 SQL 中声明这一点,因此对于 SQL 引擎和 EF 引擎来说,指示都不是那么清楚。

至于我,您呈现的行为可能表明 FirstOrDefault 被 EF 翻译器向上“传播”了一层内部查询太多,就像 Articles.GroupBy() 一样(您确定您没有放错括号吗? OrderBy? :) ) - 那将是一个错误。

但 -

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

使用像按日期这样的简单过滤器,它是一个子查询对于所有文章,按日期截断行,并消耗所有行。但是如何编写一个复杂的查询,该查询将使用多个中间排序并生成多个文章子集?哪个子集应该绑定到结果作者?所有人的联合?这将使所有顶级 where-like 子句无效。他们第一个?废话,第一个子查询往往是中间帮手。因此,很可能,当一个查询被视为一组具有相似结构的子查询时,所有这些子查询都可以作为部分加载导航属性的数据源,那么很可能只有最后一个子查询被视为实际结果。这都是抽象的想法,但它让我注意到 Take() 与 FirstOrDefault 以及它们的整体 Join 与 LeftJoin 含义实际上可以改变结果集扫描的顺序,并且,不知何故,for each author * for each title-group * select top one and check count and substitue for null这曾多次为每位作者生成少量的单项文章集,因此产生了一个结果——仅来自上次访问的标题分组。

这是我能想到的唯一解释,除了明显的“BUG!” 喊。作为 LINQ 用户,对我来说,它仍然是一个错误。要么根本不应该进行这种优化,要么它也应该包括 FirstOrDef - 因为它与 Take(1).DefaultIfEmpty() 相同。嘿,顺便说一句 - 你试过吗?正如我所说,由于 JOIN/LEFTJOIN 含义,Take(1) 与 FirstOrDefault 不同 - 但 Take(1).DefaultIfEmpty() 实际上在语义上是相同的。看看它在 SQL 中产生什么 SQL 查询以及在 EF 层中产生什么结果可能会很有趣。

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

(*)在您的情况下,我也会将 Article.AuthorID 设为导航属性(公共 Author Author 已设置),然后将查询重写为更扁平/流水线,例如:

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 对其进行测试,它只是在 JOIN 的情况下“颠倒查询”和“展平”子查询的一个示例,并且对于 LEFTJOIN 不可用,所以如果您还想查看没有文章的作者,它必须像您的原始查询一样从作者开始..

我希望这些松散的想法将有助于找到“为什么”..

于 2012-08-27T10:01:46.683 回答
2

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

于 2012-08-27T17:59:53.950 回答
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提升”中正确分支的极端案例。总而言之,在生成的 SQL 中,您在 order-by 中有 select-top-1,并且损坏看起来就像将“top 1”提升得太高了!

于 2012-08-27T16:44:36.140 回答
0

今天我才发现如果在排序子句中,它q.OrderBy(a=>a.Customer.FirstOrDefault().Name)不会命中数据库。FirstOrDefault()

于 2019-01-02T18:31:28.887 回答