我有一个数据模型,其中"顶部"对象具有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-preview1
closed-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);首先是"顶部"表的普通SELECT
FROM
,然后根据键-外键关系[Sub].[TopId] = [Top].[Id]
INNER JOIN
FROM
"顶部"表SELECT
FROM
"子"表。然后将这些查询的结果合并到内存中。
结果正是您所期望的,并且与 EF6 返回的结果非常相似:具有属性prop1
和prop2
的匿名类型'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 对象。