Jez*_*Jez 7 c# database sql-server entity-framework
我正在尝试使用GroupJoin一些数据IQueryable并将数据投影为匿名类型.我所在的原始实体GroupJoin具有ICollection导航属性(即一个:多个).我想急切加载该属性,以便我可以在组加入后访问它而不用EF返回到数据库.我知道Include()当你使用a时它不起作用GroupJoin,但是下面的代码是我发现的唯一方法,使它急于加载collection(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();
}
Run Code Online (Sandbox Code Playgroud)
如果我删除了foundRooms.ToList(),EF会在数据库中进行3次以填充我的numberOfRoles变量,但foundRooms.ToList()事实并非如此 - 它只是急于在一个查询中预先加载数据.
虽然这有效,但感觉就像完全黑客.我只是要求.ToList()让EF实际加载收集数据的副作用.如果我评论该行,它会在我尝试访问时进入数据库ContactRoomRoles.是否有一种不太常见的方式使EF eager加载导航属性?
注意:我想使用导航属性而不是将其投影到匿名类型的新属性中,因为AutoMapper Room.ContactRoomRoles在映射到DTO对象时想要访问它.
这不是一个黑客.这是一个抽象泄漏.我们应该准备好使用ORM工具(以及任何其他内部DSL)来解决抽象泄漏问题.
在ToList()你不仅执行实际的sql调用(并将数据加载到内存中)之后,还要交叉到其他Linq风格 - "Linq for objects".在此之后所有的调用Count()都不会因为你开始使用内存集合而生成sql(而不是表达式树被隐藏的IQueryable- GroupBy语句的返回类型,但带有List集合 - 返回类型的ToList).
如果没有ToList()你留下"Linq for sql",EF会将Count()IQuerybale上的每次调用转换成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();
Run Code Online (Sandbox Code Playgroud)
更新:如果您不使用可重复使用的EF工具,建议您避免使用延迟加载和手动实体加载.它们在某种意义上与linq查询相反 - 表达式树.它们是重要的(如果不是只有一个)选项,用于在"旧"平台上加载引用的实体,其中语言中没有"表达式树",但在.NET/EF中,完整查询可以"声明方式"写为表达式树而不执行(但推迟解释)应该有非常强烈的理由返回"手动"加载.
这都是关于标记为已加载或未加载的集合。
线路
foundRooms.ToList();
Run Code Online (Sandbox Code Playgroud)
(或者foundRooms.Load())
将所有Rooms 及其ContactRoomRoles集合加载到上下文中。由于Include使用了该语句,这些集合被标记为由 EF 加载。你可以通过查看来检查
context.Entry(Rooms.Local.First()).Collection(r => r.ContactRoomRoles).IsLoaded
Run Code Online (Sandbox Code Playgroud)
应该返回true.
如果省略该行foundRooms.ToList();,则每次Room.ContactRoomRoles访问集合时,EF 都会注意到它尚未标记为已加载,并将延迟加载它。之后,集合被标记为已加载,但需要额外的查询。
集合仅在以下情况下被标记为已加载:
Include-ed由语句加载Load(),如
context.Entry(Rooms.Local.First()).Collection(r => r.ContactRoomRoles).Load();
Run Code Online (Sandbox Code Playgroud)ContactRoomRoles = rm.ContactRoomRole当它是另一个属性的投影的一部分时(例如查询中的部分),则不是。
但是,在该语句之后,var roomsData = foundRooms (...).ToList()所有内容Room.ContactRoomRoles都会被填充,因为查询确实将它们加载到上下文中,并且 EF 始终执行关系修复过程,该过程会自动填充导航属性。
因此,总而言之,在查询之后,您包含带有已填充但未标记为已加载的roomsData集合的房间对象。ContactRoomRoles
知道了这一点,现在显然唯一要做的就是:防止延迟加载的发生。
实现此目的的最佳方法是阻止 EF 创建能够延迟加载的实体对象(又名代理)。您可以通过添加行来做到这一点
context.Configuration.ProxyCreationEnabled = false;
Run Code Online (Sandbox Code Playgroud)
就在using声明下方。
现在你会注意到这一行
var numberOfRoles1 = roomsData.ElementAt(1).Room.ContactRoomRoles.Count();
Run Code Online (Sandbox Code Playgroud)
不会触发额外的查询,但会返回正确的计数。