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();
假设我们有简单的 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();