假设我们有简单的一>一>多关系,具有下表结构:
public class City
{
public string Name { get; set; }
[Column("DtaCentralSchoolId")]
[ForeignKey("MyCentralSchool")]
public int? CentralSchoolId { get; set; }
public CentralSchool MyCentralSchool { get; set; }
}
public class CentralSchool
{
public string Name { get; set; }
[InverseProperty("MyCentralSchool")]
public virtual IList<Student> MyStudents { get; set; }
}
public class Student
{
public string Name { get; set; }
[Column("DtaCentralSchoolId")]
[ForeignKey("MyCentralSchool")]
public int? CentralSchoolId { get; set; }
public CentralSchool MyCentralSchool { get; set; }
}
并尝试运行以下查询:
var result = await dbContext.Set<City>()
.AsNoTracking()
.SelectMany(x => x.MyCentralSchool.MyStudents.DefaultIfEmpty(), (c, s) => new {City = c, Student = s})
.Where(x => x.Student == null || !x.Student.IsDeleted && x.Student.MyStoreId == storeId)
.FirstOrDefaultAsync();
因此,由于某种原因,CentralSchool 将生成INNERJOIN,而对于 Student 来说,有LEFT JOIN,就使用 DefaultIfEmpty((而言,这是完全没问题的。 实际上,我也希望 CentralSchool 也有LEFTJOIN,所以当没有 CentralSchool 时,无论如何都会有一些行出现在结果中,如何在当前构造中实现这一点,而无需手动重写丑陋的查询并强制LEFT JOIN出现?
更新
问题已解决,修复程序将在2.2中发布: https://github.com/aspnet/EntityFrameworkCore/issues/13511
有一件事很突出,应该在实际代码中检查:
.Where(x => x.Student == null || !x.Student.IsDeleted && x.Student.MyStoreId == storeId)
这应该是:
.Where(x => x.Student == null || (!x.Student.IsDeleted && x.Student.MyStoreId == storeId))
像这样的松散条件可能会在条件 x.Student.MyStoreId 上使 EF 跳闸,无论是否有学生,从而导致内部联接条件。
编辑:我尝试重现此问题,并且使用我的架构,查询不会加入城市到中心学校。相反,它通过CentralSchoolId FKs加入City。我怀疑您的情况的问题是数据库没有定义 FK? 数据库是通过代码优先 + 迁移设置的,还是通过数据库优先设置的?
结果查询:
SELECT TOP (1)
[Extent1].[CityId] AS [CityId],
[Extent1].[Name] AS [Name],
[Extent1].[CentralSchoolId] AS [CentralSchoolId],
[Extent2].[StudentId] AS [StudentId],
[Extent2].[Name] AS [Name1],
[Extent2].[IsDeleted] AS [IsDeleted],
[Extent2].[CentralSchoolId] AS [CentralSchoolId1]
FROM [dbo].[Cities] AS [Extent1]
LEFT OUTER JOIN [dbo].[Students2] AS [Extent2] ON [Extent2].[CentralSchoolId] = [Extent1].[CentralSchoolId]
WHERE ([Extent2].[StudentId] IS NULL) OR ([Extent2].[IsDeleted] <> 1)
注意:就我而言,我没有在学生中映射StoreId,只是IsDelete。此外,表名是 Student2,只是由于我现有的测试区域数据库中的名称冲突。
实体定义与您的定义相同,只是映射了 PK,并将 IsDelete 添加到学生。
public class City
{
[Key]
public int CityId { get; set; }
public string Name { get; set; }
[ForeignKey("MyCentralSchool")]
public int? CentralSchoolId { get; set; }
public virtual CentralSchool MyCentralSchool { get; set; }
}
public class CentralSchool
{
[Key]
public int CentralSchoolId { get; set; }
public string Name { get; set; }
[InverseProperty("MyCentralSchool")]
public virtual IList<Student> MyStudents { get; set; }
}
[Table("Students2")]
public class Student
{
[Key]
public int StudentId { get; set; }
public string Name { get; set; }
public bool IsDeleted { get; set; }
[ForeignKey("MyCentralSchool")]
public int? CentralSchoolId { get; set; }
public virtual CentralSchool MyCentralSchool { get; set; }
}
测试表达式运行:
var result = context.Set<City>()
.AsNoTracking()
.SelectMany(x => x.MyCentralSchool.MyStudents.DefaultIfEmpty(), (c, s) => new { City = c, Student = s })
.Where(x => x.Student == null || !x.Student.IsDeleted)
.FirstOrDefault();
我也异步运行了它,并生成了相同的查询。针对 SQL Server 使用 EF6 运行。
编辑 2:确认 EF6 和 EF Core 之间的查询生成存在差异。在解析城市和学生之间的关系时,EF Core 会在城市和中心学校之间生成内部联接,其中 EF 6 通过公共 FK 联接表来优化这一点。我会考虑将其作为 EF Core 中的潜在错误提出。
鉴于您想要所有活跃学生及其关联学生的列表,以及包括所有没有活跃学生的城市(因此也将列出所有城市(
在 EF Core 中返回匹配结果的解决方法(尽管很丑陋(:
var result2 = context.Set<City>()
.AsNoTracking()
.SelectMany(x => x.MyCentralSchool.MyStudents.DefaultIfEmpty(), (c, s) => new { City = c, Student = s })
.Where(x => !x.Student.IsDeleted)
.Union(context.Set<City>().AsNoTracking().Where(x => x.MyCentralSchool == null || !x.MyCentralSchool.MyStudents.Any(s => !s.IsDeleted))
.Select(x => new { City = x, Student = (Student)null }))
.ToList();