Search code examples
hibernatesubqueryjpqlquerydslcase-when

antlr.NoViableAltException for subquery in then-clause with Querydsl / JPQL / Hibernate


I have three entities in my application, Customer, PriceLevel, and TemporalCustomerPriceLevel. Each Customer has a field called defaultPriceLevel, which references PriceLevel directly. Temporarily, Customers can be switched to an alternative PriceLevel. For this purpose, an entry to TemporalCustomerPriceLevel is added that references both the PriceLevel and the Customer.

The entity classes are:

@Entity
public class Customer implements Serializable {
...
  @ManyToOne
  @JoinColumn(name = "default_price_level_id")
  private PriceLevel defaultPriceLevel;

  @OneToMany(mappedBy = "customer", cascade = CascadeType.ALL, orphanRemoval = true)
  private List<TemporalCustomerPriceLevel> temporalCustomerPriceLevels
      = new ArrayList<TemporalCustomerPriceLevel>();
...
}

@Entity
public class PriceLevel implements Serializable {
  ...
  @OneToMany(mappedBy = "priceLevel", cascade = CascadeType.ALL, orphanRemoval = true)
  private List<TemporalCustomerPriceLevel> temporalCustomerPriceLevels;

  @OneToMany(mappedBy = "defaultPriceLevel", cascade = CascadeType.PERSIST)
  private List<Customer> customers;
  ...
}

@Entity
public class TemporalCustomerPriceLevel implements Serializable {
  ...
  @ManyToOne
  @JoinColumn(name = "customer_id")
  private Customer customer;

  @ManyToOne(cascade = CascadeType.PERSIST)
  @JoinColumn(name = "price_level_id")
  private PriceLevel priceLevel;
  ...
}

I now want to query for the active price level, i. e. defaultPriceLevel if no (active, left out for simplicity) TemporalCustomerPriceLevel exists, and the PriceLevel referenced by TemporalCustomerPriceLevel otherwise.

I use Spring, JPA (Hibernate), MySql and Querydsl. In Querydsl, I have written the following:

QCustomer customer = QCustomer.customer;
QTemporalCustomerPriceLevel qt = QTemporalCustomerPriceLevel.temporalCustomerPriceLevel;

SimpleExpression<PriceLevel> cases = new CaseBuilder()
    .when(JPAExpressions.select(qt.count()).from(qt).where(qt.customer.eq(customer)).eq(1L))
    .then(
        JPAExpressions.select(qt.priceLevel).from(qt).where(qt.customer.eq(customer))
        )
    .otherwise(
        JPAExpressions.select(customer.defaultPriceLevel).from(customer)
        ).as("activePriceLevel");

JPAQuery<Tuple> query = factory.select(customer, cases).from(customer);

This results in an error:

antlr.NoViableAltException: unexpected AST node: query
at org.hibernate.hql.internal.antlr.HqlSqlBaseWalker.expr(HqlSqlBaseWalker.java:1367) [hibernate-core-4.3.11.Final.jar:4.3.11.Final]
...

The query produced by Querydsl looks like this:

select customer, 
    (case when ((select count(temporalCustomerPriceLevel)
        from TemporalCustomerPriceLevel temporalCustomerPriceLevel
        where temporalCustomerPriceLevel.customer = customer) = ?1) 
    then (select temporalCustomerPriceLevel.priceLevel
        from TemporalCustomerPriceLevel temporalCustomerPriceLevel
        where temporalCustomerPriceLevel.customer = customer) 
    else (select customer.defaultPriceLevel
        from Customer customer) 
    end) as activePriceLevel
from Customer customer

This seems to make sense. Further, if I replace the two JPAExpressions lines by fixed PriceLevel objects, the query runs as expected.

So the main question is: Is there any constraint on using subqueries in the then-clause of a case block? Anything wrong with my Querydsl? Any help is very appreciated.


Solution

  • I have worked out that the following alternative query solves my problem. Still, if anyone knows about hibernate and JPQL subqueries in then-clauses, I would still be interested.

    JPAQueryFactory factory = new JPAQueryFactory(em);
    QCustomer customer = QCustomer.customer;
    QPriceLevel priceLevel = QPriceLevel.priceLevel;
    QTemporalCustomerPriceLevel tmp = new QTemporalCustomerPriceLevel("tmp_qtcpl");
    Date now = new Date();
    
    JPAQuery<Tuple> query = factory.select(customer, priceLevel).from(customer, priceLevel).where(
        JPAExpressions.select(tmp).from(tmp)
        .where(tmp.validAfter.loe(now)
            .and(tmp.validUntil.after(now)
                .and(tmp.priceLevel.eq(priceLevel)
                    .and(tmp.customer.eq(customer))))).exists()
        .or(
          customer.defaultPriceLevel.eq(priceLevel)
          .and(
              JPAExpressions.select(tmp).from(tmp)
              .where(tmp.validAfter.loe(now)
                  .and(tmp.validUntil.after(now)
                      .and(tmp.customer.eq(customer)))).notExists()
              )
          )
        );