我正在使用EF 4.3.1 ...刚刚升级到4.4(问题仍然存在),其中包含 EF 4.x DbContext Generator 。我有以下数据库名为'Wiki'(用于创建表和数据的SQL脚本是here):
编辑wiki文章时,新版本将作为新记录插入而不是更新其记录,并且修订计数器会递增。在我的数据库中有一位作者“John Doe”,它有两篇文章,“文章A”和“文章B”,其中文章A有两个版本(1和2),但文章B只有一个版本。
我同时禁用了延迟加载和代理创建(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()))
这会产生错误的结果,并只检索第一篇文章:
通过将.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差别不大(比较:SQL for .FirstOrDefault() vs SQL for .Take(1)),但它们都返回正确的结果:
.FirstOrDefault()
.Take(1)
(重新排列列顺序以便于比较)
因此,罪魁祸首不是生成的SQL,而是EF对结果的解释。为什么EF将第一个结果解释为单个Article
实例,而它将第二个结果解释为两个Article
个实例?为什么第一个查询返回不正确的结果?
编辑:我在Connect上打开了bug report。如果您认为解决此问题很重要,请提前注册。
答案 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的解释,我得到了猜测'由于尝试在SQL中模拟延迟评估,使用Take的查询可能会减少行数,而您的情况则表明它是另一种方式!我确实理解为什么你会观察到这种效果。
它可能只是特定于实现的..对我来说,Take(1)和FirstOrDefault可能看起来都像TOP 1
,但从功能的角度来看,它们的&可能会略有不同。 #39;懒惰':一个函数可以评估所有元素并返回第一个,第二个可以先评估然后返回它并打破评估。它只是一个"提示"可能发生的事情。对我来说,这是一个废话,因为我没有看到关于这个主题的文档,一般来说我确定Take / FirstOrDefault都是懒惰的,并且应该只评估前N个元素。
在查询的第一部分中,group.Select + orderBy + TOP1是"清除指示"你感兴趣的是具有最高价值的单行'在每个组的一列中 - 但事实上,有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
执行,其中多次为每个作者生成小的单项文章集合,从而导致一个结果 - 仅来自最后一次访问的标题组。
这是我能想到的唯一解释,除了明显的" BUG!"喊。作为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对其进行测试,这只是一个将查询翻转过来的示例'并且'展平'在JOIN的情况下子查询并且对于LEFTJOIN是不可用的,所以如果您还想查看没有文章的作者,它必须从作者开始,就像您的原始查询一样。
我希望这些松散的想法能帮助我找到原因' ..
答案 1 :(得分:2)
FirstOrDefault()
方法是即时的,而另一个方法(Take(int)
)推迟到执行。
答案 2 :(得分:0)
正如在前面的回答中我试图解释这个问题 - 我辞职了,我正在写另一个:)再看一遍后,我认为这是一个错误。我认为您应该使用Take并将案例发布到Microsoft的Connect,并检查他们对此有何看法。
“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()
将不会对数据库产生影响。