EF Core嵌套Linq在N + 1个SQL查询中选择结果

我有一个数据模型,其中’Top’对象有0到N’Sub’对象。 在SQL中,这是通过外键dbo.Sub.TopId

 var query = context.Top //.Include(t => t.Sub) Doesn't seem to do anything .Select(t => new { prop1 = t.C1, prop2 = t.Sub.Select(s => new { prop21 = s.C3 //C3 is a column in the table 'Sub' }) //.ToArray() results in N + 1 queries }); var res = query.ToArray(); 

在Entity Framework 6(延迟加载关闭)中,此Linq查询将转换为单个 SQL查询。 结果将被完全加载,因此res[0].prop2将是已经填充的IEnumerable

使用EntityFrameworkCore(NuGet v1.1.0)时,子集合尚未加载并且类型为:

 System.Linq.Enumerable.WhereSelectEnumerableIterator<Microsoft.EntityFrameworkCore.Storage.ValueBuffer, f__AnonymousType1>. 

在迭代数据之前,不会加载数据,从而导致N + 1个查询。 当我向查询添加.ToArray() (如注释中所示),使用SQL分析器将数据完全加载到var res ,但是显示这不再在1个SQL查询中实现。 对于每个“Top”对象,执行“Sub”表上的查询。

首先指定.Include(t => t.Sub)似乎没有任何改变。 匿名类型的使用似乎也不是问题,用new MyPocoClass { ... }替换new { ... }new MyPocoClass { ... }不会改变任何东西。

我的问题是: 有没有办法让行为类似于EF6,所有数据都立即加载?


注意 :我意识到在这个例子中,问题可以通过执行查询在内存中创建匿名对象来解决,如下所示:

 var query2 = context.Top .Include(t => t.Sub) .ToArray() .Select(t => new //... select what is needed, fill anonymous types 

然而,这只是一个例子,我确实需要创建对象作为Linq查询的一部分,因为AutoMapper使用它来填充我的项目中的DTO


更新:使用新的EF Core 2.0测试,问题仍然存在。 (21-08-2017)

aspnet/EntityFrameworkCore GitHub repo: 问题4007上跟踪问题

更新:一年后,此问题已在版本2.1.0-preview1-final得到修复。 (2018年3月1日)

更新: EF版本2.1已发布,它包含一个修复程序。 看下面的答案。 (2018年5月31日)

对于里程碑2.1.0-preview1 ,GitHub问题#4007已标记为closed-fixed 。 现在2.1预览1已经在NuGet上提供,正如这篇.NET博客文章中所讨论的那样。

版本2.1也已发布,使用以下命令安装它:

 Install-Package Microsoft.EntityFrameworkCore.SqlServer -Version 2.1.0 

然后在嵌套的.Select(x => ...) .ToList()上使用.ToList()来指示应该立即获取结果。 对于我原来的问题,这看起来像这样:

 var query = context.Top .Select(t => new { prop1 = t.C1, prop2 = t.Sub.Select(s => new { prop21 = s.C3 }) .ToList() // <-- Add this }); var res = query.ToArray(); // Execute the Linq query 

这导致在数据库上运行2个SQL查询(而不是N + 1); 首先是一个普通的SELECT FROM 'Top'表,然后是一个SELECT FROM '表,其中INNER JOIN FROM 'Top'表,基于Key-ForeignKey关系[Sub].[TopId] = [Top].[Id] 。 然后将这些查询的结果合并到内存中。

结果正如您所期望的那样,与EF6将返回的内容非常相似:匿名类型的数组'a具有属性prop1prop2 ,其中prop2是具有属性prop2的匿名类型'b的列表。 最重要的是, 所有这些都是在.ToArray()调用之后完全加载的!

我遇到了同样的问题。

您提出的解决方案不适用于相对较大的表。 如果您查看生成的查询,那么它将是一个没有where条件的内连接。

var query2 = context.Top .Include(t => t.Sub).ToArray().Select(t => new // …选择需要的内容,填写匿名类型

我通过重新设计数据库解决了这个问题,但我很乐意听到更好的解决方案。

在我的例子中,我有两个表A和B.表A与B一对多。当我试图用你所描述的列表直接解决它时我没有设法做到(.NET的运行时间) LINQ为0.5秒,而.NET Core LINQ在运行30秒后失败)。

因此,我必须为表B创建一个外键,并从表B的一侧开始,没有内部列表。

 context.A.Where(a => aBID == 1).ToArray(); 

之后,您可以简单地操作生成的.NET对象。