EF Core 2.1,强制SelectMany产生LEFT JOIN

时间:2018-10-02 12:47:10

标签: entity-framework linq entity-framework-6 entity-framework-core ef-fluent-api

假设我们具有以下表结构的简单 one-> one->许多关系:

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

因此出于某种原因会生成Centralstrong INNER JOIN ,而对于Student,有 LEFT JOIN ,就使用DefaultIfEmpty()。 实际上,我也希望CentralSchool也能获得 LEFT JOIN ,因此,如果没有CentralSchool,无论如何结果中都会有一些行,我如何才能在当前构造中实现这一点而无需手动重写难看的查询并强制左加入出现?

更新
该问题已解决,修复程序将在 2.2 中发布: https://github.com/aspnet/EntityFrameworkCore/issues/13511

1 个答案:

答案 0 :(得分:1)

一件事很突出,应该在真实代码中进行检查:

.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跳闸,无论是否有学生,都会导致内部联接条件。

编辑:我尝试重现此问题,并且使用我的架构,查询未在City to Central School上加入。而是将City连入CentralSchoolId FK到学生小瓶。我怀疑您遇到的问题是数据库没有定义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。另外,由于我现有的测试区域数据库中的名称冲突,表名是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();

我也运行了Async,并且生成了相同的查询。在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();