带有 CONCAT_WS 函数的 JPA CriteriaBuilder 抛出 NullPointerException

Posted

技术标签:

【中文标题】带有 CONCAT_WS 函数的 JPA CriteriaBuilder 抛出 NullPointerException【英文标题】:JPA CriteriaBuilder with CONCAT_WS function throws NullPointerException 【发布时间】:2020-11-17 19:55:22 【问题描述】:

我有以下用于过滤订单的 CriteriaQuery。

CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<OrderReducedDTO> cq = cb.createQuery(OrderReducedDTO.class);

Root<Order> root = cq.from(Order.class);
Join<Order, Customer> joinCustomer = root.join(Order_.customer);
Join<Order, Shipment> joinShipment = root.join(Order_.shipment);
Join<Shipment, Carrier> joinCarrier = joinShipment.join(Shipment_.carrier);
Join<Order, Payment> joinPayment = root.join(Order_.payment);
Join<Payment, PaymentMethod> joinPaymentMethod = joinPayment.join(Payment_.paymentMethod);
Join<Shipment, Country> joinCountry = joinShipment.join(Shipment_.country);

cq.select(cb.construct(
        OrderReducedDTO.class,
        root.get(Order_.id),
        root.get(Order_.incrementId),
        root.get(Order_.state),
        root.get(Order_.couponCode),
        root.get(Order_.totalDiscount),
        root.get(Order_.total),
        root.get(Order_.originChannel),
        root.get(Order_.branchOffice),
        joinCarrier.get(Carrier_.carrierCode),
        cb.function("CONCAT_WS", String.class,
                cb.literal(","),
                joinShipment.get(Shipment_.streetName),
                joinShipment.get(Shipment_.streetNumber),
                joinShipment.get(Shipment_.city),
                joinCountry.get(Country_.name),
                joinShipment.get(Shipment_.zipCode)
        ),
        joinPaymentMethod.get(PaymentMethod_.code),
        joinPayment.get(Payment_.paymentDate),
        root.get(Order_.createdAt),
        root.get(Order_.updatedAt),
        root.get(Order_.externalId),
        joinCustomer.get(Customer_.fullName)
));
... filters and predicates...

给我带来麻烦并导致抛出 NPE 的部分是这个

cb.function("CONCAT_WS", String.class,
                    cb.literal(","),
                    joinShipment.get(Shipment_.streetName),
                    joinShipment.get(Shipment_.streetNumber),
                    joinShipment.get(Shipment_.city),
                    joinCountry.get(Country_.name),
                    joinShipment.get(Shipment_.zipCode)
            )

更具体地说,当我使用CONCAT_WS 函数时。如果我使用CONCAT,它可以工作。 这是我得到的堆栈跟踪:

java.lang.NullPointerException: null
at org.hibernate.hql.internal.NameGenerator.generateColumnNames(NameGenerator.java:27)
at org.hibernate.hql.internal.ast.util.SessionFactoryHelper.generateColumnNames(SessionFactoryHelper.java:434)
at org.hibernate.hql.internal.ast.tree.SelectClause.initializeColumnNames(SelectClause.java:270)
at org.hibernate.hql.internal.ast.tree.SelectClause.finishInitialization(SelectClause.java:260)
at org.hibernate.hql.internal.ast.tree.SelectClause.initializeExplicitSelectClause(SelectClause.java:255)
at org.hibernate.hql.internal.ast.HqlSqlWalker.useSelectClause(HqlSqlWalker.java:1026)
...

这是我的 OrderReducedDTO

@Getter
public class OrderReducedDTO 

    @JsonProperty("order_id")
    private Integer orderId;

    @JsonProperty("increment_id")
    private String incrementId;

    private OrderStates state;

    @JsonProperty("coupon_code")
    private String couponCode;

    @JsonProperty("total_discount")
    private BigDecimal totalDiscount;

    private BigDecimal total;

    @JsonProperty("origin_channel")
    private String originChannel;

    @JsonProperty("branch_office")
    private String branchOffice;

    @JsonProperty("shipping_method")
    private String shippingMethod;

    @JsonProperty("shipping_address")
    private String shippingAddress;

    @JsonProperty("payment_method")
    private String paymentMethod;

    @JsonProperty("payment_date")
    private Timestamp paymentDate;

    @JsonProperty("created_at")
    private Timestamp createdAt;

    @JsonProperty("updated_at")
    private Timestamp updatedAt;

    @JsonProperty("external_id")
    private String externalId;

    @JsonProperty("customer_full_name")
    private String customerFullName;

    @Setter
    private List<OrderProductReducedDTO> products;

    public OrderReducedDTO(Integer orderId,
                           String incrementId,
                           OrderStates state,
                           String couponCode,
                           BigDecimal totalDiscount,
                           BigDecimal total,
                           String originChannel,
                           String branchOffice,
                           String shippingMethod,
                           String shippingAddress,
                           String paymentMethod,
                           Object paymentDate,
                           Object createdAt,
                           Object updatedAt,
                           String externalId,
                           String customerFullName) 
        this.orderId = orderId;
        this.incrementId = incrementId;
        this.state = state;
        this.couponCode = couponCode;
        this.totalDiscount = totalDiscount;
        this.total = total;
        this.originChannel = originChannel;
        this.branchOffice = branchOffice;
        this.shippingMethod = shippingMethod;
        this.shippingAddress = shippingAddress;
        this.paymentMethod = paymentMethod;
        this.paymentDate = (Timestamp) paymentDate;
        this.createdAt = (Timestamp) createdAt; //https://hibernate.atlassian.net/browse/HHH-4179
        this.updatedAt = (Timestamp) updatedAt;
        this.externalId = externalId;
        this.customerFullName = customerFullName;
    

我主要想知道的是我是否正确使用了function 方法。我想我是因为CONCAT 有效。

【问题讨论】:

【参考方案1】:

在 Hibernate 中进行了数小时的调试后,我终于找到了问题的根源:

org/hibernate/hql/internal/ast/tree/ConstructorNode.java

private Type[] resolveConstructorArgumentTypes() throws SemanticException 
    SelectExpression[] argumentExpressions = this.collectSelectExpressions();
    if (argumentExpressions == null) 
        return new Type[0];
     else 
        Type[] types = new Type[argumentExpressions.length];

        for(int x = 0; x < argumentExpressions.length; ++x) 
            types[x] = argumentExpressions[x].getDataType();
        

        return types;
    

argumentExpressions[x].getDataType() 正在返回 null

我搜索了一下,发现这可能是由于 Hibernate 不知道给定 SQL 函数的实际返回类型(显然它只知道最常见的返回类型)。 然后我跟随this answer 并实现了一个自定义MetadataBuilderContributor,如下所示:

public class SqlFunctionsMetadataBuilderContributor implements MetadataBuilderContributor 

    @Override
    public void contribute(MetadataBuilder metadataBuilder) 
        metadataBuilder.applySqlFunction(
                "concat_ws",
                new StandardSQLFunction("concat_ws", StandardBasicTypes.STRING)
        );
    

我在 application.properties 上添加了:

spring.jpa.properties.hibernate.metadata_builder_contributor=ar.com.glamit.glamitoms.config.hibernate.SqlFunctionsMetadataBuilderContributor

重新启动应用后,argumentExpressions[x].getDataType() 现在返回 StringType,而 NullPointerException 消失了。

【讨论】:

以上是关于带有 CONCAT_WS 函数的 JPA CriteriaBuilder 抛出 NullPointerException的主要内容,如果未能解决你的问题,请参考以下文章

mysql中concat_ws()函数

hive - concat_ws 函数

SQL常用函数

带有 Hikari 的 Spring JPA 未释放连接

在JPA中是否有与INSTR Oracle函数相同的函数

如何按CONCAT_WS分组