繁体   English   中英

如何通过GroupJoin使EF eager加载集合导航属性?

[英]How to make EF eager load a collection navigation property through a GroupJoin?

我正在尝试使用IQueryable GroupJoin一些数据并将数据投影到匿名类型。 我是GroupJoin的原始实体具有ICollection导航属性(即one:many)。 我想急切加载该属性,以便我可以在组加入后访问它而不用EF返回到数据库。 我知道当你使用GroupJoinInclude()不起作用,但是下面的代码是我发现让它急切加载集合的唯一方法( 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 eager加载导航属性?

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

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

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

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

没有办法避免这种情况,否则在一个复杂查询中计算服务器端的所有count(*)值。 如果您尝试使用Linq编写此类查询(构建expression tree ) - 您将再次遇到抽象泄漏。 ORM工具旨在将对象映射到“RDBS实体”,保持CRUD(创建读取更新删除)操作 - 如果语句变得更复杂 - 您将无法预见生成的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中,完整查询可以“声明方式”写为表达式树而不执行(但推迟解释)应该有非常强烈的理由返回“手动”加载。

这些都是关于标记为已加载或未加载的集合。

这条线

foundRooms.ToList();

(或foundRooms.Load()

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

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

哪个应该返回true

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

集合仅在标记为已加载时 -

  • Include -ed
  • 通过延迟加载加载
  • Load()语句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();

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

这称为抽象泄漏 ,它意味着您的抽象暴露了一些实现细节。

当你调用.ToList()并在Linq到sqlLinq到对象之间切换(我不喜欢单词cross)时会发生这种情况。

我建议你阅读“漏洞抽象法则”以便更好地掌握,因为单脚解释起来相当复杂。

它背后的主要思想是,一切都会按计划运行,但是当你试图提供底层不可靠层的完整抽象时,它会慢一些,但有时,层会通过抽象泄漏,你会感觉到抽象可以做的事情。非常保护你免受伤害。


编辑澄清:

调用ToList()强制linq-to-entities评估并将结果作为列表返回。

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

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

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

换句话说,在你调用ToList()的那一刻,它就被懒惰地执行了。

ToList()调用之前,C#不进行SQL调用。 实际上,它不是内存操作。

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM