EF Core 2.1,强制 SelectMany 产生 LEFT JOIN

EF Core 2.1, force SelectMany to produce LEFT JOIN

假设我们有简单的 one->one->many 关系,表结构如下:

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 INNER JOIN 正在生成,而对于 Student 则有 LEFT JOIN,这完全是很好,就使用 DefaultIfEmpty() 而言。 实际上,我希望 CentralSchool 也使用 LEFT JOIN,所以当没有 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))

无论是否有学生,像这样的松散条件都可能使 EF 在条件 x.Student.MyStoreId 上跳闸,从而导致内部连接条件。

编辑:我尝试重现这个问题,但在我的架构中,查询没有加入城市到中央学校。相反,它通过 CentralSchoolId FK 将 City 连接到 Student vial。我怀疑你的情况的问题是数据库没有定义 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,只是 IsDeleted。此外,table 名称是 Student2,只是因为我现有的测试区域数据库中存在名称冲突。

实体定义与您的相同,除了映射了 PK 并将 IsDeleted 添加到 Student。

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();

我 运行 它也是异步的,并且生成了相同的查询。 运行 使用 EF6 针对 SQL 服务器。

编辑 2:确认 EF6 和 EF Core 之间的查询生成存在差异。在解决 City 和 Student 之间的关系时,EF Core 确实会在 City 和 Central School 之间产生内部连接,其中 EF 6 通过公共 FK 连接 table 来对此进行优化。我会考虑将其作为 EF Core 中的潜在错误提出。

鉴于您需要所有活跃学生及其关联学生的列表,加上所有没有活跃学生的城市(因此所有城市也会被列出)

returnEF 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();