我正在尝试使用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
。
答案 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调用。所以实际上,这不是内存操作。
所以,是的,它将数据作为上下文的一部分放入内存中并在相同的上下文中读取。