使用GROUP BY的JPA Projections查询不能与空字段一起使用

问题描述

我想复制一个MysqL和H2中都能运行的查询

SELECT 
    pedido_linha.produto_id AS material,COALESCE(pedido_linha.unidade_medida_id,"UN") AS uom,pedido_linha.data_pedido  AS referenceDate,SUM(COALESCE(pedido_linha.quantidade,0)) AS totalQuantity,SUM(COALESCE(pedido_linha.valor_total,0)) AS totalGross,0) - COALESCE(pedido_linha.valor_descontos,0) - 
        COALESCE(pedido_linha.valor_impostos,0)) AS totalNet,SUM(COALESCE(pedido_linha.valor_custo,0)) AS totalCogs
FROM pedido_linha
GROUP BY pedido_linha.produto_id,pedido_linha.data_pedido,"UN");

作为示例,即使某些“分组依据”字段为空,查询也可以在MysqL中正常运行:

MySQL query output

已进行以下尝试,以提取JPA封闭投影中的分组结果:

public interface AggregatedByMaterialUOMDate {

Produto getMaterial();
UnidadeMedida getUom();
LocalDate getReferenceDate();

Float getTotalQuantity();
Float getTotalGross();
Float getTotalNet();
Float getTotalCogs();

}

伴随存储库查询

@Repository
public interface PedidoLinhaRepository extends JpaRepository<PedidoLinha,PedidoLinha.PedidoLinhaCompositeKey> {

    @Query("SELECT pl.produto AS material,"
            + "COALESCE(pl.unidadeMedida,:unidadeMedidapadrao) AS uom,"
            + "pl.dataPedido  AS referenceDate,"
            + "SUM(COALESCE(pl.quantidade,"
            + "SUM(COALESCE(pl.valorTotal,0) - COALESCE(pl.valorDescontos,0) - COALESCE(pl.valorImpostos,"
            + "SUM(COALESCE(pl.valorCusto,0)) AS totalCogs "
            + "FROM PedidoLinha pl "
            + "GROUP BY pl.produto,pl.dataPedido,COALESCE(pl.unidadeMedida,:unidadeMedidapadrao)")
    List<AggregatedByMaterialUOMDate> consolidatedSelloutByMaterialUOMDayAtLocation(@Param("unidadeMedidapadrao") UnidadeMedida unidadeMedidapadrao);
}

上面的JPQL查询返回一个包含0个元素的列表,因此与先前显示的本机查询不一致。

我们试图将COALESCE(pl.unidadeMedida,:unidadeMedidapadrao)更改为简单的pl.unidadeMedida(在SELECT和GROUP BY子句中),但没有成功。

仅当从两个子句中完全删除对pl.unidadeMedida的所有引用时,查询才成功返回所有值。

下面是PedidoLinha(在查询中由pl引用)类的示例,其中显示了对UnidadeMedida的引用:

@Getter
@Setter
@NoArgsConstructor
@requiredArgsConstructor
@EqualsAndHashCode(of = "pedidoLinhaCompositeKey")
@Entity
public class PedidoLinha {

    @EmbeddedId
    @NonNull // torna campo obrigatório e parâmetro do construtor gerado pelo @Data (lombok)
    private PedidoLinhaCompositeKey pedidoLinhaCompositeKey;

    @Data // lombok: @ToString,@EqualsAndHashCode,@Getter on all fields @Setter on all non-final fields,and @requiredArgsConstructor
    @NoArgsConstructor
    @requiredArgsConstructor
    @Embeddable
    @EqualsAndHashCode
    public static class PedidoLinhaCompositeKey implements Serializable {

        @NonNull // torna campo obrigatório e parâmetro do construtor gerado pelo @Data (lombok)
        private String id;
        
        @ManyToOne(optional = false,fetch = FetchType.LAZY)
        @NonNull // torna campo obrigatório e parâmetro do construtor gerado pelo @Data (lombok)
        private Pedido pedido;
                
    }
    
    @ManyToOne(optional = false)
    private Produto produto;
    
    @ManyToOne
    private UnidadeMedida unidadeMedida;

    public UnidadeMedida getUnidadeMedida() {
        return (unidadeMedida == null) ? new UnidadeMedida("UN") : unidadeMedida;
    }

    // rest of the entity code
}

UnidadeMedida类

@Getter
@Setter
@EqualsAndHashCode(of = "id")
@NoArgsConstructor
@Entity
public class UnidadeMedida {

    @Id
    private String id;

    private String descricao;
    
    public UnidadeMedida(String id) {
        this.id = id;
    }
    
}

可能会发生什么?我相信这可能是一个Hibernate错误,但找不到对此问题的任何引用。

解决方法

找出JPQL查询未产生任何记录的关键原因是在数据库调用中进行隐式联接的方式。

以下代码表示运行上面的JPQL查询后在数据库(How to show the last queries executed on MySQL?)中执行的实际SQL代码:

SELECT 
    pedidolinh0_.produto_id as col_0_0_,coalesce(pedidolinh0_.unidade_medida_id,'UN') as col_1_0_,pedidolinh0_.data_pedido as col_2_0_,sum(coalesce(pedidolinh0_.quantidade,0)) as col_3_0_,sum(coalesce(pedidolinh0_.valor_total,0)) as col_4_0_,0)-coalesce(pedidolinh0_.valor_descontos,0)-coalesce(pedidolinh0_.valor_impostos,0)) as col_5_0_,sum(coalesce(pedidolinh0_.valor_custo,0)) as col_6_0_,produto1_.id as id1_88_,produto1_.ativo as ativo2_88_,produto1_.data_descontinuacao as data_des3_88_,produto1_.data_introducao as data_int4_88_,produto1_.descricao as descrica5_88_,produto1_.ean as ean6_88_,produto1_.lote_minimo_requisicoes as lote_min7_88_,produto1_.multiplo_requisicoes as multiplo8_88_,produto1_.ncm as ncm9_88_,produto1_.unidade_medida_padrao_id as unidade10_88_ 
from pedido_linha pedidolinh0_ 
    inner join produto produto1_ on pedidolinh0_.produto_id=produto1_.id 
    cross join unidade_medida unidademed2_ 
    cross join pedido pedido3_ 
where pedidolinh0_.unidade_medida_id=unidademed2_.id
group by 
    pedidolinh0_.produto_id,pedidolinh0_.data_pedido,'UN')

问题在于,对SQL的转换会在查询中自动包含cross join + WHERE子句pedidolinh0_.unidade_medida_id=unidademed2_.id,实际上忽略了该字段为null的每个实例。 即使coalescing语句中使用了select,这些记录也已被where子句忽略,因此无效。

解决方案是在JPQL查询中强制左联接,如下所示:

SELECT 
    pl.produto AS material,COALESCE(um,:unidadeMedidaPadrao) AS uom,pl.dataPedido AS referenceDate,SUM(COALESCE(pl.quantidade,0)) AS totalQuantity,SUM(COALESCE(pl.valorTotal,0)) AS totalGross,0) - COALESCE(pl.valorDescontos,0) - COALESCE(pl.valorImpostos,0)) AS totalNet,SUM(COALESCE(pl.valorCusto,0)) AS totalCogs
FROM PedidoLinha pl
    LEFT JOIN pl.unidadeMedida um
GROUP BY pl.produto,pl.dataPedido,pl.unidadeMedida

结果,在数据库中执行的最终SQL中,cross join语句被left outer join替换,并且不包含在where子句中。

对于为什么在Hibernate中做出此设计决策(使用交叉联接+ where语句代替左联接)的任何见解,将不胜感激。