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



我有一个数据模型,其中"顶部"对象具有0到N个"子"对象。在 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();

在实体框架 6(关闭延迟加载)中,此 Linq 查询将转换为单个SQL 查询。结果将是完全加载的,因此res[0].prop2将是一个已经填充的IEnumerable<SomeAnonymousType>

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

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

在循环访问数据之前,不会加载数据,从而导致 N + 1 个查询。当我向查询添加.ToArray()时(如注释所示),数据将完全加载到var res中,但是使用 SQL 探查器表明这不再在 1 个 SQL 查询中实现。对于每个"顶部"对象,将执行对"子"表的查询。

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

我的问题是:有没有办法获得类似于 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 进行测试时,仍然存在问题。(2017-08-21)

在 GitHub 存储库aspnet/EntityFrameworkCore跟踪问题:问题 4007

更新:一年后,此问题已在版本2.1.0-preview1-final中修复。(2018-03-01)

更新:EF 版本 2.1 已发布,它包括一个修复程序。 请参阅下面的答案。(2018-05-31)

GitHub 问题 #4007 已被标记为里程碑2.1.0-preview1closed-fixed。现在,NuGet 上提供了 2.1 预览版 1,如此 .NET 博客文章中所述。

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

Install-Package Microsoft.EntityFrameworkCore.SqlServer -Version 2.1.0

然后在嵌套.Select(x => ...)上使用.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);首先是"顶部"表的普通SELECTFROM,然后根据键-外键关系[Sub].[TopId] = [Top].[Id]INNER JOINFROM"顶部"表SELECTFROM"子"表。然后将这些查询的结果合并到内存中。

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

我遇到了同样的问题。

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

var query2 = 上下文。返回页首 .包括(t => t.Sub) .ToArray() .选择(t => new//...选择所需内容,填写匿名类型

我通过重新设计数据库解决了它,尽管我很高兴听到更好的解决方案。

就我而言,我有两个表 A 和 B。 表 A 与 B 具有一对多。当我尝试按照您的描述直接使用列表解决它时,我没有设法做到这一点(.NET LINQ 的运行时间为 0.5 秒,而 .NET Core LINQ 在运行时间 30 秒后失败)。

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

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

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

最新更新