3

I have the following CriteriaQuery that I use to filter orders.

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...

The part that's giving me trouble and causing a NPE to be thrown is this

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)
            )

More, specifically, when I use the CONCAT_WS function. If I use CONCAT, it works. This is the stacktrace I get:

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)
...

This is my 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;
    }
}

What I mainly want to know is if I'm using the function method correctly. I assume I am because CONCAT works.

MorganGlam
  • 129
  • 2
  • 11

1 Answers1

2

After hours of debugging within Hibernate, I finally arrived at the root of the problem:

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() was returning null.

I googled and found out that this could be caused by Hibernate not knowing the actual return type of the given SQL function (apparently it only knows the most common ones). I then followed this answer and implemented a custom MetadataBuilderContributor like so:

public class SqlFunctionsMetadataBuilderContributor implements MetadataBuilderContributor {

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

And on my application.properties I added:

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

After relaunching the app, argumentExpressions[x].getDataType() now returns a StringType and the NullPointerException is gone.

MorganGlam
  • 129
  • 2
  • 11