19

我有一个数据模型,其中“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()到查询中(如注释中所示)时,数据被完全加载到var res,但是使用 SQL 分析器显示这不再在 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 存储库上跟踪问题:问题 4007

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

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

4

2 回答 2

9

GitHub 问题#4007已标记closed-fixed为里程碑2.1.0-preview1。现在 2.1 preview1 已在NuGet上可用,如本.NET 博客文章中所述。

版本 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的“Top”表,然后是一个SELECT FROM带有“Top”表的“Sub”INNER JOIN FROM表,基于 Key-ForeignKey 关系[Sub].[TopId] = [Top].[Id]。然后将这些查询的结果合并到内存中。

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

于 2018-03-01T08:43:51.053 回答
1

我遇到了同样的问题。

您提出的解决方案不适用于相对较大的表。如果您查看生成的查询,它将是一个没有 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 => a.B.ID == 1).ToArray();

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

于 2017-04-14T09:37:17.850 回答