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

时间:2018-10-11 18:02:36

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

我正在尝试使用GroupJoin IQueryable来一些数据并将这些数据投影为匿名类型。我GroupJoin所在的原始实体具有ICollection导航属性(即one:many)。我想加载该属性,以便在组加入后可以访问它,而无需EF返回数据库。我知道Include()在您使用GroupJoin时不起作用,但是以下代码是我发现使其渴望加载集合(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渴望加载该导航属性?

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

3 个答案:

答案 0 :(得分:5)

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

ToList()之后,您不仅可以执行实际的sql调用(并将数据加载到内存中),而且还可以使用其他Linq风格-“对象的Linq”。此后,您对Count()的所有调用不会仅由于您开始在内存集合中工作而生成SQL(而不是在表达式树中使用,它们被IQueryable隐藏-返回类型) GroupBy语句,但具有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中将引用的实体加载到“旧”平台上,而在.NET / EF中,完全查询可以作为声明树“声明式”编写而无需执行(但推迟了解释),应该有非常充分的理由返回“手动”加载。

答案 1 :(得分:3)

所有与是否标记为已加载的集合有关。

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()语句加载的
  • ,如

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

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

但是,在语句var roomsData = foundRooms (...).ToList()之后,所有Room.ContactRoomRoles都填充了 ,因为查询确实将它们加载到上下文中,并且EF始终执行 relationship fixup < / em>进程,该进程会自动填充导航属性。

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

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

实现此目标的最佳方法是阻止EF创建能够进行延迟加载的实体对象,也就是代理。您可以通过添加一行来实现

context.Configuration.ProxyCreationEnabled = false;

using语句的下方。

现在您会注意到该行

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

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

答案 2 :(得分:1)

这称为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调用。所以实际上,这不是内存操作。

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