You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

PostgreSQL Left Join在ManyToOne属性为Null时丢失行的问题

问题:JPQL查询添加关联属性后丢失NULL值记录

场景还原

在Spring Boot + PostgreSQL项目中,原本的JPQL查询可以正常从ConfigElement表获取数据并映射到ConfigurationReview实体:

@Query(
          "SELECT new xx.xx.xx.dao.ConfigurationReview(r.id, MAX(ce.id) AS configElementId,"
        + " r.checkMethod, r.targetDate, r.endDate, r.sessionDate, SUM(CASE WHEN i.criticality ="
        + " 0 AND i.relation = 0 THEN 1 ELSE 0 END) AS majorFindings, SUM(CASE WHEN i.criticality"
        + " = 1 AND i.relation = 0 THEN 1 ELSE 0 END) AS minorFindings, r.state) FROM"
        + " ConfigElement ce "
        + "LEFT JOIN ce.reviews r ON r.configElement.id = ce.id "
        + "LEFT JOIN r.criteria cr ON cr.review.id = r.id "
        + "LEFT JOIN cr.issues i ON i.criterion.id = cr.id "
        + "WHERE ce.id = :configurationId AND r.id IS NOT NULL GROUP BY r.id")
  Page<ConfigurationReview> findConfigurationReviews(
          @Param("configurationId") Long configurationId, Pageable pageable);

当给ConfigurationReview的构造函数添加r.milestone(ManyToOne关联属性)后,查询出现异常:仅返回milestone非NULL的记录,milestone为NULL的记录被过滤。现有2条非NULL、2条NULL的记录,查询仅返回2条。

修改后的查询:

@Query(
          "SELECT new xx.xx.xx.dao.ConfigurationReview(r.id, MAX(ce.id) AS configElementId,"
        + " r.checkMethod, r.targetDate, r.endDate, r.sessionDate, SUM(CASE WHEN i.criticality ="
        + " 0 AND i.relation = 0 THEN 1 ELSE 0 END) AS majorFindings, SUM(CASE WHEN i.criticality"
        + " = 1 AND i.relation = 0 THEN 1 ELSE 0 END) AS minorFindings, r.state, r.milestone) FROM"
        + " ConfigElement ce "
        + "LEFT JOIN ce.reviews r ON r.configElement.id = ce.id "
        + "LEFT JOIN r.criteria cr ON cr.review.id = r.id "
        + "LEFT JOIN cr.issues i ON i.criterion.id = cr.id "
        + "WHERE ce.id = :configurationId AND r.id IS NOT NULL GROUP BY r.id")
  Page<ConfigurationReview> findConfigurationReviews(
          @Param("configurationId") Long configurationId, Pageable pageable);

ConfigurationReview中milestone的定义:

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(foreignKey = @ForeignKey(name = "fk_review_milestones_on_milestone_id"))
private Milestone milestone;

原因分析

JPQL要求SELECT中的非聚合字段必须全部出现在GROUP BY子句中。当你在SELECT中加入r.milestone(实体对象),但GROUP BY仅包含r.id时,PostgreSQL的分组逻辑会对NULL值的milestone进行错误处理,导致对应记录被排除。

解决办法

方案1:将r.milestone加入GROUP BY

修改GROUP BY子句,把r.milestone也包含进去:

@Query(
          "SELECT new xx.xx.xx.dao.ConfigurationReview(r.id, MAX(ce.id) AS configElementId,"
        + " r.checkMethod, r.targetDate, r.endDate, r.sessionDate, SUM(CASE WHEN i.criticality ="
        + " 0 AND i.relation = 0 THEN 1 ELSE 0 END) AS majorFindings, SUM(CASE WHEN i.criticality"
        + " = 1 AND i.relation = 0 THEN 1 ELSE 0 END) AS minorFindings, r.state, r.milestone) FROM"
        + " ConfigElement ce "
        + "LEFT JOIN ce.reviews r ON r.configElement.id = ce.id "
        + "LEFT JOIN r.criteria cr ON cr.review.id = r.id "
        + "LEFT JOIN cr.issues i ON i.criterion.id = cr.id "
        + "WHERE ce.id = :configurationId AND r.id IS NOT NULL GROUP BY r.id, r.milestone")
  Page<ConfigurationReview> findConfigurationReviews(
          @Param("configurationId") Long configurationId, Pageable pageable);

方案2:用聚合函数包裹r.milestone

由于每个r.id对应唯一的milestone(每个Review只有一个关联的Milestone),可以用MAX()或MIN()聚合函数包裹r.milestone,这样无需修改GROUP BY:

@Query(
          "SELECT new xx.xx.xx.dao.ConfigurationReview(r.id, MAX(ce.id) AS configElementId,"
        + " r.checkMethod, r.targetDate, r.endDate, r.sessionDate, SUM(CASE WHEN i.criticality ="
        + " 0 AND i.relation = 0 THEN 1 ELSE 0 END) AS majorFindings, SUM(CASE WHEN i.criticality"
        + " = 1 AND i.relation = 0 THEN 1 ELSE 0 END) AS minorFindings, r.state, MAX(r.milestone)) FROM"
        + " ConfigElement ce "
        + "LEFT JOIN ce.reviews r ON r.configElement.id = ce.id "
        + "LEFT JOIN r.criteria cr ON cr.review.id = r.id "
        + "LEFT JOIN cr.issues i ON i.criterion.id = cr.id "
        + "WHERE ce.id = :configurationId AND r.id IS NOT NULL GROUP BY r.id")
  Page<ConfigurationReview> findConfigurationReviews(
          @Param("configurationId") Long configurationId, Pageable pageable);

补充说明

两种方案都能解决NULL值记录丢失的问题:

  • 方案1严格遵循JPQL的GROUP BY规范,适合需要明确分组逻辑的场景;
  • 方案2利用聚合函数的特性,无需修改GROUP BY,代码改动更小。

内容的提问来源于stack exchange,提问作者bullgr

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.05 11:50:20