This is very unlikely to be a bug in Hibernate. There was a technical mistake in fabricating the criteria query given. Taking the same example but in a simpler form.
Let's assume that we are interested in generating the following SQL query.
SELECT
p.prod_id,
p.prod_name,
CASE
WHEN sum(r.rating_num)/count(DISTINCT r.rating_id) IS NULL THEN 0
ELSE round(sum(r.rating_num)/count(DISTINCT r.rating_id))
END AS avg_rating
FROM
product p
LEFT OUTER JOIN
rating r
ON p.prod_id=r.prod_id
GROUP BY
p.prod_id,
p.prod_name
HAVING
CASE
WHEN sum(r.rating_num)/count(DISTINCT r.rating_id) IS NULL THEN 0
ELSE round(sum(r.rating_num)/count(DISTINCT r.rating_id))
END>=1
Based on the following table in MySQL.
mysql> desc rating;
+-------------+---------------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-------------+---------------------+------+-----+---------+----------------+
| rating_id | bigint(20) unsigned | NO | PRI | NULL | auto_increment |
| prod_id | bigint(20) unsigned | YES | MUL | NULL | |
| rating_num | int(10) unsigned | YES | | NULL | |
| ip_address | varchar(45) | YES | | NULL | |
| row_version | bigint(20) unsigned | NO | | 0 | |
+-------------+---------------------+------+-----+---------+----------------+
5 rows in set (0.08 sec)
This table rating
has an obvious many-to-one relationship with another table product
(prod_id
is the foreign key referencing the primary key prod_id
in the product
table).
In this question, we are only interested in the CASE
construct in the HAVING
clause.
The following criteria query,
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
CriteriaQuery<Tuple> criteriaQuery = criteriaBuilder.createTupleQuery();
Root<Product> root = criteriaQuery.from(entityManager.getMetamodel().entity(Product.class));
ListJoin<Product, Rating> prodRatingJoin = root.join(Product_.ratingList, JoinType.LEFT);
List<Expression<?>> expressions = new ArrayList<Expression<?>>();
expressions.add(root.get(Product_.prodId));
expressions.add(root.get(Product_.prodName));
Expression<Integer> sum = criteriaBuilder.sum(prodRatingJoin.get(Rating_.ratingNum));
Expression<Long> count = criteriaBuilder.countDistinct(prodRatingJoin.get(Rating_.ratingId));
Expression<Number> quotExpression = criteriaBuilder.quot(sum, count);
Expression<Integer> roundExpression = criteriaBuilder.function("round", Integer.class, quotExpression);
Expression<Integer> selectExpression = criteriaBuilder.<Integer>selectCase().when(quotExpression.isNull(), criteriaBuilder.literal(0)).otherwise(roundExpression);
expressions.add(selectExpression);
criteriaQuery.multiselect(expressions.toArray(new Expression[0]));
expressions.remove(expressions.size() - 1);
criteriaQuery.groupBy(expressions.toArray(new Expression[0]));
criteriaQuery.having(criteriaBuilder.greaterThanOrEqualTo(selectExpression, criteriaBuilder.literal(1)));
List<Tuple> list = entityManager.createQuery(criteriaQuery).getResultList();
for (Tuple tuple : list) {
System.out.println(tuple.get(0) + " : " + tuple.get(1) + " : " + tuple.get(2));
}
Generates the following correct SQL query as expected.
select
product0_.prod_id as col_0_0_,
product0_.prod_name as col_1_0_,
case
when sum(ratinglist1_.rating_num)/count(distinct ratinglist1_.rating_id) is null then 0
else round(sum(ratinglist1_.rating_num)/count(distinct ratinglist1_.rating_id))
end as col_2_0_
from
projectdb.product product0_
left outer join
projectdb.rating ratinglist1_
on product0_.prod_id=ratinglist1_.prod_id
group by
product0_.prod_id ,
product0_.prod_name
having
case
when sum(ratinglist1_.rating_num)/count(distinct ratinglist1_.rating_id) is null then 0
else round(sum(ratinglist1_.rating_num)/count(distinct ratinglist1_.rating_id))
end>=1
For the technical perspective, look at the following line in the above criteria query.
criteriaQuery.having(criteriaBuilder.greaterThanOrEqualTo(selectExpression, criteriaBuilder.literal(1)));
Its analogous line in the question was written like following.
createQuery.having(criteriaBuilder.greaterThanOrEqualTo(selectExpression, 1));
See the original expression in the question doing the exact same thing :
Expression<Integer> selectExpression = criteriaBuilder.<Integer>selectCase()
.when(quotExpression.isNull(), 0)
.<Integer>otherwise(roundExpression);
This expression was attempted to be passed to criteriaBuilder.greaterThanOrEqualTo()
as follows.
criteriaQuery.having(criteriaBuilder.greaterThanOrEqualTo(selectExpression, 0));
Pay special attention to the second parameter to greaterThanOrEqualTo()
above. It is 0
. It should have been criteriaBuilder.literal(0)
instead hence, the exception as mentioned in the question.
Thus, always insist upon using CriteriaBuilder#literal(T value)
for literal values whenever necessary as done above while using expressions in the CriteriaBuilder#selectCase()
construct.
Tested on Hibernate 4.3.6 final, Hibernate 5.0.5 final alternatively. I will try to run the same query on EclipseLink (2.6.1 final) later on. There should not be a quirk anymore.
EclipseLink has no problem at all with the modified version of the query except that it requires an Object
type parameter to the constructor argument (formal parameter), if constructor expressions are used in place of Tuple
which this question has nothing to do with after all. This is a long-standing bug in EclipseLink still to be fixed - an analogous example.