EF Core Inner join 改为 Left

Posted

技术标签:

【中文标题】EF Core Inner join 改为 Left【英文标题】:EF Core Inner join instead Left 【发布时间】:2021-09-29 18:01:29 【问题描述】:

我使用Include 的查询生成带有Inner 连接而不是 Left 的sql。我的 FK 可以为空,所以我无法解释这种行为。使用可为空的 FK,我期望正常的 Left 加入。

我错过了什么吗?

Linq 查询:

     var projectEntity = await _context.Projects
            // few more includes were omitted
            .Include(p => p.Invoice)
            .FirstOrDefaultAsync(c => c.ProjectId == id);

类:

[Table("InvoicedHistory")]
public class InvoiceHistory

    [Key]
    [Column("InvoicedHistory_ID")]
    public int InvoicedHistoryId  get; set; 
    // few properties omitted
    [Column("Project_ID")]
    public int? ProjectId  get; set; 


public class Project

    public int ProjectId  get; set; 
    
    // few properties were omitted
    
    [ForeignKey(nameof(InvoiceHistory.ProjectId))]
    public virtual InvoiceHistory Invoice  get; set; 

项目类也使用fluent api:

        modelBuilder.Entity<Project>(entity =>
        
            entity.ToTable("Projects");

            entity.HasKey(e => e.ProjectId)
                .HasName("PK_Project_Project_ID_Project");

           // few statements were omitted

        );

生成的Sql:(很难清理这个查询。它包含几个连接来包含我省略的属性的数据)

    SELECT [t].[Project_ID], [t].[Project_Client], [t].[Project_IrsDate], [t].[Project_Name], [t].[Client_ID], [t].[Client_Name], [t].[InvoicedHistory_ID], [t].[DateSubmitted], [t].[Project_ID0], [t0].[Debitor_ID], [t0].[Project_ID], [t0].[Debitor_ID0], [t0].[Address_Number], [t0].[Alias], [t0].[Alpha_Name], [t0].[Co], [t0].[Country_ID], [t0].[Currency_ID], [t0].[Havi_YesOrNo]
  FROM (
      SELECT TOP(1) [p].[Project_ID], [p].[Project_Client], [p].[Project_IrsDate], [p].[Project_Name], [c].[Client_ID], [c].[Client_Name], [i].[InvoicedHistory_ID], [i].[DateSubmitted], [i].[Project_ID] AS [Project_ID0]
      FROM [Projects] AS [p]
      INNER JOIN [Clients] AS [c] ON [p].[Project_Client] = [c].[Client_ID]
      INNER **<<<<<<<<(expect LEFT)** JOIN [InvoicedHistory] AS [i] ON [p].[Project_ID] = [i].[InvoicedHistory_ID]
      WHERE [p].[Project_ID] = 19922
  ) AS [t]
  LEFT JOIN (
      SELECT [p0].[Debitor_ID], [p0].[Project_ID], [d].[Debitor_ID] AS [Debitor_ID0], [d].[Address_Number], [d].[Alias], [d].[Alpha_Name], [d].[Co], [d].[Country_ID], [d].[Currency_ID], [d].[Havi_YesOrNo]
      FROM [ProjectDebitors] AS [p0]
      INNER JOIN [Debitors] AS [d] ON [p0].[Debitor_ID] = [d].[Debitor_ID]
  ) AS [t0] ON [t].[Project_ID] = [t0].[Project_ID]
  ORDER BY [t].[Project_ID], [t].[Client_ID], [t].[InvoicedHistory_ID], [t0].[Debitor_ID], [t0].[Project_ID], [t0].[Debitor_ID0]

看看这一行——

   INNER <<<<<<<<(expect LEFT)<<<<<< JOIN [InvoicedHistory] AS [i] ON [p].[Project_ID] = [i].[InvoicedHistory_ID]

内连接使我的查询不返回任何内容,因为我没有发票信息。如果我手动将其替换为 Left 连接,则 sql 查询将返回所有必要的数据。

【问题讨论】:

@PanagiotisKanavos 我还是想加载项目实体。如果我没有发票,我希望看到空的相应属性。以目前的行为,我得到了没有发票的项目的空结果。 模型中没有任何内容表明Invoice 是可选的。底层字段可能为空,但 Project 的 模型中没有任何内容表明这一点。 `公共int?项目 ID 获取;放; ` 表示InvoiceHistory 记录可能没有对应的项目,而不是项目有可选的发票 @PanagiotisKanavos 是否可以在不指定'public int? InvoiceId get;set;' 在项目模型中并且没有 Fluent api? >>> 是否可以保留当前的项目模型并仅使用属性? 您是否使用可空引用类型?根据One-to-One Relations,该属性应该是可选的。如果使用 NRT,InvoiceHistory Invoice 表示需要 Invoice 【参考方案1】:

我认为你可以使用 Fluent API 来获得你想要的结果:

modelBuilder.Entity<Project>()
  .HasOne(p => p.Invoice)
  .WithOne()
  .HasForeignKey(ih => ih.ProjectId);

这应该将其更改为左连接,因为我们没有指定 .IsRequired()

如下SO Answer - Equivalent for .HasOptional in Entity Framework Core 1 (EF7)中提到

在 EF 7 中找不到等效方法。按照惯例,CLR 类型可以包含 null 的属性将被配置为可选。因此,决定关系是否可选的决定因素是 FK 属性是否可以分别为空。

如果你的 FK 属性是像 int 这样的值类型,你应该将它声明为可空(int?)。

现在您的注释问题很可能是以下内容没有按照您的想法进行:

[ForeignKey(nameof(InvoiceHistory.ProjectId))]

//Does not resolve to:
[ForeignKey("InvoiceHistory.ProjectId")]

//Does resolve to:
[ForeignKey("ProjectId")]

现在,即使这是您要查找的内容,ForeignKey 检测的操作顺序也是先检查父类型,然后再检查属性类型。

public class InvoiceHistory

    public int? ProjectId  get; set; 


public class Project

    public int ProjectId  get; set; 
    
    // this is pointing to Project.ProjectId
    // and Project.ProjectId is not nullable
    // so the join becomes an inner join
    // and really only works because they both have the same name
    [ForeignKey(nameof(InvoiceHistory.ProjectId))]
    public virtual InvoiceHistory Invoice  get; set; 

如果您希望它指向属性类型,您需要重命名 InvoiceHistory 名称:

public class InvoiceHistory

    public int? ProjectFk  get; set; 


public class Project

    public int ProjectId  get; set; 
    
    // this is pointing to InvoiceHistory.ProjectFk 
    // because there is no Project.ProjectFk 
    [ForeignKey(nameof(InvoiceHistory.ProjectFk))]
    public virtual InvoiceHistory Invoice  get; set; 

EntityFramework Data Annotations

如果您想看到它创建错误的 SQL,您可以这样做:

public class InvoiceHistory

    public int? ProjectId  get; set; 


public class Project

    public int ProjectFk  get; set; 
    
    [ForeignKey("ProjectFk")]
    public virtual InvoiceHistory Invoice  get; set; 

EF 将创建:

 INNER JOIN [InvoicedHistory] AS [i] ON [p].[Project_ID] = [i].[ProjectFk]

并且会导致带有类似Invalid column name 的消息的SqlException。

【讨论】:

@AntonPutau 更新了更好的解释。

以上是关于EF Core Inner join 改为 Left的主要内容,如果未能解决你的问题,请参考以下文章

分页改为inner join的方式

NETEZZA:LEFT JOIN 能比 INNER JOIN 快吗?

在SQL联表查询的时候,in和inner join各有啥优点?

EF Core 中DbContext不会跟踪聚合方法和Join方法返回的结果

利用EF Core的Lambda表达式和Join进行多表查询

利用EF Core的Lambda表达式和Join进行多表查询