【问题标题】:How to make EF eager load a collection navigation property through a GroupJoin?如何使 EF 通过 GroupJoin 急切加载集合导航属性?
【发布时间】:2019-03-16 21:48:51
【问题描述】:

我正在尝试使用IQueryable GroupJoin 一些数据并将该数据投影到匿名类型中。我 GroupJoining 进入的原始实体具有 ICollection 导航属性(即一:多)。我想急切地加载该属性,以便在组加入后访问它,而无需 EF 返回数据库。我知道当你使用GroupJoin 时Include() 不起作用,但下面的代码是我发现让它急切加载集合的唯一方法(ContactRoomRoles):

using (var context = new MyDbContext()) {
    var foundRooms = context.Rooms.Include(rm => rm.ContactRoomRoles);
    foundRooms.ToList();  // <-- Required to make EF actually load ContactRoomRoles data!

    var roomsData = foundRooms
        .GroupJoin(
            context.Contacts,
            rm => rm.CreatedBy,
            cont => cont.Id,
            (rm, createdBy) => new {
                ContactRoomRoles = rm.ContactRoomRoles,
                Room = rm,
                CreatedBy = createdBy.FirstOrDefault()
            }
        )
        .ToList();

    var numberOfRoles1 = roomsData.ElementAt(1).Room.ContactRoomRoles.Count();
    var numberOfRoles2 = roomsData.ElementAt(2).Room.ContactRoomRoles.Count();
    var numberOfRoles3 = roomsData.ElementAt(3).Room.ContactRoomRoles.Count();
}

如果我删除 foundRooms.ToList(),EF 会 3 次进入数据库以在最后填充我的 numberOfRoles 变量,但使用 foundRooms.ToList() 它不会 - 它只是预先在一个查询中预先加载数据.

虽然这行得通,但感觉就像是一个彻头彻尾的黑客攻击。我只是打电话给.ToList() 是为了让 EF 实际加载集合数据的副作用。如果我注释掉该行,它会在我尝试访问ContactRoomRoles 的任何时候进入数据库。有没有更简单的方法让 EF 急切加载导航属性?

注意:我想使用导航属性而不是将其投影到匿名类型的新属性中,因为 AutoMapper 想要在映射到 DTO 对象时访问 Room.ContactRoomRoles。

【问题讨论】:

  • 我觉得你在使用 ORM 的同时抱怨代码看起来很hacky,这很有趣。如果你想看到一些丑陋的代码,你应该对数据库进行跟踪并查看它生成的 SQL 代码!
  • 如果您这样做只是为了获得 AutoMapper 生成的 DTO 图,为什么不让 AutoMapper 使用可查询扩展功能作为查询的一部分为您进行投影? docs.automapper.org/en/stable/Queryable-Extensions.html
  • 非 hacky 方法是定义和使用正确的导航属性而不是 GroupJoin。 EF 中的任何手动连接都表明实体模型不正确。在您的情况下,ContactRoomRole 实体应具有指向 Contact 的导航属性,表示 CreatedBy FK。
  • @IvanStoev 实际查询更复杂,需要组加入,因为我有一个要加入的子查询。这个例子只是简化了。
  • 那么可能会提供更现实的例子。急切加载的规则很明确 - 请参阅 stackoverflow.com/questions/52767559/…。您在当前示例中使用的内容称为在查询返回内存中的实体的具体化过程中的跟踪和导航属性修复。

标签: c# sql-server database entity-framework


【解决方案1】:

这不是黑客攻击。这是一个抽象泄漏。我们应该准备好使用 ORM 工具(和任何其他内部 DSL)来解决抽象泄漏问题。

在ToList() 之后,您不仅可以执行实际的 sql 调用(并将数据加载到内存中),还可以使用其他 Linq 风格 - “Linq for objects”。在此之后,您对 Count() 的所有调用不会仅仅因为您开始使用内存集合(而不是 表达式树 那些被 IQueryable 隐藏的 - GroupBy 的返回类型)而生成 sql声明,但使用 List 集合 - 返回类型为 ToList)。

如果没有ToList(),您将继续使用“Linq for sql”,EF 会将 IQuerybale 上 Count() 的每个调用转换为 sql;三个 Conut() 调用 = 三个带下划线的 Sql 语句。

没有办法避免这种情况,否则在一个复杂的查询中计算服务器端的所有count(*) 值。如果您尝试使用 Linq(构造 expression tree)编写此类查询 - 您将再次遇到抽象泄漏。 ORM 工具旨在将对象映射到与 CRUD(创建读取更新删除)操作保持一致的“RDBS 实体”——如果语句变得更复杂——您将无法预见生成的 sql(以及所有运行时异常,例如“无法生成 sql”对于这样的linq')。因此,不要将 linq 用于复杂的“类似报告”的查询(在某些情况下可以——这取决于您的重用要求和测试可能性)。使用旧的好 SQL 并通过 ADO 或 EF ADO “sql 扩展”调用它,例如 EF Core FromSql:

var blogs = context.Blogs
    .FromSql("EXECUTE dbo.GetMostPopularBlogsForUser {0}", user)
    .ToList();

更新:如果您不使用可重用的 EF 工具,最好避免使用延迟加载和手动实体加载。它们在某种意义上与 linq 查询相反——表达式树。它们是实现在“旧”平台上加载引用实体的重要(如果不仅仅是一个)选项,在这些平台上没有语言中的“表达式树”,但在 .NET/EF 中,完整的查询可以“声明性方式”编写为表达式树而无需执行(但延迟解释)应该有非常充分的理由返回“手动”加载。

【讨论】:

  • List&lt;T&gt; 也可以通过.AsQueryable() 扩展方法转换为IQueryable。问题是应该了解每个 LINQ 调用中使用了IQueryable 的哪个实现(EF 查询提供程序或列表包装器)。也调用 EF IQueryable 像 .ToList(),实际执行 SQL,在文档中称为物化。
  • 不是 CRUD(创建读取更新删除)吗?
【解决方案2】:

这都是关于是否标记为已加载的集合。

线

foundRooms.ToList();

(或foundRooms.Load())

将所有Rooms 及其ContactRoomRoles 集合加载到上下文中。由于使用了Include 语句,这些集合被标记为由EF 加载。您可以通过查看来检查

context.Entry(Rooms.Local.First()).Collection(r => r.ContactRoomRoles).IsLoaded

应该返回true。

如果省略foundRooms.ToList(); 行,每次访问Room.ContactRoomRoles 集合时,EF 会注意到它尚未标记为已加载,并将延迟加载它。之后,集合被标记为已加载,但需要进行额外的查询。

一个集合仅在它被标记为已加载时-

  • Include-ed
  • 通过延迟加载加载
  • 由Load() 语句加载,如

    context.Entry(Rooms.Local.First()).Collection(r => r.ContactRoomRoles).Load();
    

当它是投影到另一个属性的一部分时(例如查询中的 ContactRoomRoles = rm.ContactRoomRole 部分),则不是。

但是,在语句 var roomsData = foundRooms (...).ToList() 之后,所有 Room.ContactRoomRoles 都填充,因为查询确实将它们加载到上下文中,并且 EF 总是执行关系修复过程,它会自动填充导航属性。

因此,总而言之,在您的查询之后,roomsData 包含带有 ContactRoomRoles 集合的房间对象,这些集合已填充,但未标记为已加载。

知道了这一点,很明显现在唯一要做的就是:防止发生延迟加载。

实现这一点的最佳方法是防止 EF 创建能够延迟加载的实体对象,即 代理。你可以通过添加行来做到这一点

context.Configuration.ProxyCreationEnabled = false;

就在using 语句的下方。

现在你会注意到这条线

var numberOfRoles1 = roomsData.ElementAt(1).Room.ContactRoomRoles.Count();

不会触发额外的查询,但会返回正确的计数。

【讨论】:

  • 您说“知道这一点,现在很明显,唯一要做的就是:防止发生延迟加载。” - 但是,我不能只使用.Load() 语句让EF​​ 加载ContactRoomRoles 集合吗?为什么我还需要防止延迟加载?
  • 您必须为每个room 单独Load() 所有集合。
  • 这是正确答案。 @Jez,看看Applying filters when explicitly loading related entities 文档部分。忽略过滤器部分:当使用 Query 方法时,通常最好关闭导航属性的延迟加载。这是因为否则整个集合可能会在执行过滤查询之前或之后由延迟加载机制自动加载。
【解决方案3】:

这称为Abstraction Leak,这意味着您的抽象公开了一些实现细节。

当您调用 .ToList() 并在 Linq to sql 和 Linq to objects 之间切换(我不喜欢交叉这个词)时,就会发生这种情况。

我建议您阅读The Law of Leaky Abstractions 以更好地掌握,因为单脚解释非常复杂。

其背后的主要思想是,当您尝试提供底层不可靠层的完整抽象时,一切都会按计划工作,但比平常慢,但有时,该层会通过抽象泄漏,您会感觉到抽象不能完全保护你。


编辑澄清:

调用 ToList() 强制 linq-to-entities 评估并以列表形式返回结果。

意思是,例如从上面的答案:

var blogs = context.Blogs
    .FromSql("EXECUTE dbo.GetMostPopularBlogsForUser {0}", user)
    .ToList();

将评估到相应的上下文模型——博客模型。

也就是说,它在你调用ToList()的那一刻被延迟执行。

在ToList() 调用之前,C# 不执行 SQL 调用。所以实际上,它不是内存中的操作。

所以是的,它将数据作为上下文的一部分放入内存并在同一上下文中读取。

【讨论】:

  • 这是一个很好的答案,但我想更详细地了解为什么 EF 正在这样做,以便我更好地了解发生了什么。当我调用.ToList() 时,它是将数据作为上下文的一部分放入内存还是什么?然后它会稍后在相同的上下文中读取该数据吗?在我看来,如果这样做,他们肯定会实施适当的机制来实现这种行为。
  • 编辑解释:)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-06-16
  • 1970-01-01
  • 1970-01-01
  • 2015-02-21
  • 2018-11-06
相关资源
最近更新 更多