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

时间:2017-01-10 16:23:22

标签: c# sql-server linq entity-framework-core select-n-plus-1

我有一个数据模型,其中'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<SomeAnonymousType>已填充。

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

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

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

首先指定.Include(t => t.Sub)似乎没有任何改变。使用匿名类型似乎也不是问题,用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回购:Issue 4007

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

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

2 个答案:

答案 0 :(得分:7)

对于里程碑closed-fixed,GitHub问题#4007已被标记为2.1.0-preview1。现在,NuGet已经在.NET Blog post上提供了2.1预览1。

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

Install-Package Microsoft.EntityFrameworkCore.SqlServer -Version 2.1.0

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

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'热门'表格,然后是SELECT FROM'子'表,其中INNER JOIN FROM为' Top'表,基于Key-ForeignKey关系[Sub].[TopId] = [Top].[Id]。然后将这些查询的结果合并到内存中。

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

答案 1 :(得分:1)

我遇到了同样的问题。

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

  

var query2 = context.Top       .Include(t =&gt; t.Sub)       .ToArray()       。选择(t =&gt;新// ...选择需要的内容,填写匿名类型

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

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

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

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

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