EF中Include/ThenInclude与可空导航属性的实践疑问及优化咨询
我写了一段数据查询代码,为了在Clinic导航属性为空时仍能调用ThenInclude(),用了空合并运算符??创建虚拟Clinic对象替代,但我觉得这个写法不太好,也不确定它是否有效。我的问题如下:
- 在
Include()内部使用new()在当前及未来EF版本中是否为良好/安全的实践? - 在
ThenInclude()内部使用new()在当前及未来EF版本中是否为良好/安全的实践? - 是否有更优写法,无需拆分多查询或延迟加载
clinic!.Base与clinic!相关属性?
注:返回实体为只读,无变更追踪。CreateBaseQueryable方法仅返回DbContext的IQueryable对象,此处未提供其代码。
List<Child> children = await CreateBaseQueryable(context, healthBoardId) .Include(child => child.ChildStatuses) .Include(child => child.ChildClinics) .ThenInclude(childClinic => childClinic.Clinic ?? new Clinic()) .ThenInclude(clinic => clinic!.Base) .Include(child => child.ChildClinics) .ThenInclude(childClinic => childClinic.Clinic ?? new Clinic()) .ThenInclude(clinic => clinic!.Board) .Where(child => child.ChildStatuses.Any(status => status.ACounter >= 2)) .Where(child => child.ChildClinics.Any( clinic => !clinic.IsDeleted && clinic.Clinic != null && ( clinic.Clinic.ClinicType == (int)ClinicType.Type1|| clinic.Clinic.ClinicType == (int)ClinicType.Type2) ) ) .ToListAsync();
问题解答
1. Include()内部使用new()的实践安全性
这绝对不是EF Core推荐的做法。Include的作用是定义数据库关联数据的加载路径,本质是告诉EF要生成包含关联表的SQL查询。而new()创建的是内存对象,EF无法将它映射到数据库的关联关系上,这种写法属于未定义行为——当前版本可能碰巧能运行,但后续EF版本的查询解析逻辑一旦调整,大概率会出现SQL生成错误或查询结果异常,完全没有安全性可言。
2. ThenInclude()内部使用new()的实践安全性
和上面的问题本质一致,ThenInclude是基于前面的关联路径继续加载深层关联,它需要明确知道当前路径对应的数据库实体和外键关系。虚拟的new Clinic()无法提供这些元数据,EF在解析时可能会出现逻辑混乱,要么生成无效SQL,要么实际根本没加载到Base/Board数据。同样属于不安全、不规范的写法,存在严重的版本兼容性风险。
3. 更优写法推荐
你的查询已经通过Where条件过滤掉了clinic.Clinic == null的记录(childClinic.Clinic != null),所以所有符合条件的ChildClinics的Clinic属性肯定不为空。基于这个前提,完全可以去掉空合并运算符,直接用空值抑制运算符!告诉编译器“我确定这个属性不为空”:
List<Child> children = await CreateBaseQueryable(context, healthBoardId) .Include(child => child.ChildStatuses) .Include(child => child.ChildClinics) .ThenInclude(childClinic => childClinic.Clinic!) .ThenInclude(clinic => clinic.Base) .Include(child => child.ChildClinics) .ThenInclude(childClinic => childClinic.Clinic!) .ThenInclude(clinic => clinic.Board) .Where(child => child.ChildStatuses.Any(status => status.ACounter >= 2)) .Where(child => child.ChildClinics.Any( clinic => !clinic.IsDeleted && clinic.Clinic != null && ( clinic.Clinic.ClinicType == (int)ClinicType.Type1|| clinic.Clinic.ClinicType == (int)ClinicType.Type2) ) ) .AsNoTracking() // 明确指定无变更追踪,符合只读需求 .ToListAsync();
这种写法完全符合EF Core的查询规范,没有兼容性风险,也不需要拆分查询或启用延迟加载。如果担心后续Where条件被修改导致Clinic为空的情况漏进来,还可以在Include时直接过滤ChildClinics:
.Include(child => child.ChildClinics.Where(cc => cc.Clinic != null)) .ThenInclude(childClinic => childClinic.Clinic!) .ThenInclude(clinic => clinic.Base)
这样能确保加载的ChildClinics都是Clinic不为空的,进一步规避空引用风险。
内容的提问来源于stack exchange,提问作者Scott

