以下条件查询计算不同产品组的评级平均值。
CriteriaBuilder criteriaBuilder=entityManager.getCriteriaBuilder();
CriteriaQuery<Tuple>criteriaQuery=criteriaBuilder.createQuery(Tuple.class);
Metamodel metamodel=entityManager.getMetamodel();
EntityType<Product>entityType=metamodel.entity(Product.class);
Root<Product>root=criteriaQuery.from(entityType);
SetJoin<Product, Rating> join = root.join(Product_.ratingSet, JoinType.LEFT);
Expression<Number> quotExpression = criteriaBuilder.quot(criteriaBuilder.sum(join.get(Rating_.ratingNum)), criteriaBuilder.count(join.get(Rating_.ratingNum)));
Expression<Integer> roundExpression = criteriaBuilder.function("round", Integer.class, quotExpression);
Expression<Object> selectExpression = criteriaBuilder.selectCase().when(quotExpression.isNull(), 0).otherwise(roundExpression );
criteriaQuery.select(criteriaBuilder.tuple(root.get(Product_.prodId).alias("prodId"), selectExpression.alias("rating")));
criteriaQuery.groupBy(root.get(Product_.prodId));
criteriaQuery.having(criteriaBuilder.greaterThanOrEqualTo(roundExpression, 0));
criteriaQuery.orderBy(criteriaBuilder.desc(root.get(Product_.prodId)));
TypedQuery<Tuple> typedQuery = entityManager.createQuery(criteriaQuery);
List<Tuple> tuples = typedQuery.getResultList();
它生成以下SQL查询:
SELECT product0_.prod_id AS col_0_0_,
CASE
WHEN Sum(ratingset1_.rating_num) / Count(ratingset1_.rating_num) IS
NULL THEN
0
ELSE Round(Sum(ratingset1_.rating_num) / Count(ratingset1_.rating_num))
END AS col_1_0_
FROM social_networking.product product0_
LEFT OUTER JOIN social_networking.rating ratingset1_
ON product0_.prod_id = ratingset1_.prod_id
GROUP BY product0_.prod_id
HAVING Round(Sum(ratingset1_.rating_num) / Count(ratingset1_.rating_num)) >= 0
ORDER BY product0_.prod_id DESC
case...when
结构将null
值替换为0
,如果case
子句中的指定表达式计算为null
。
我需要在case...when
子句中使用相同的having
构造,以便可以通过将group by
替换为{{1}来过滤null
子句返回的行组。在0
构造计算的值列表中,如果有的话。
因此,case...when
子句应该像
having
有可能,如果在HAVING
(CASE
WHEN Sum(ratingset1_.rating_num)/Count(ratingset1_.rating_num) IS
NULL THEN 0
ELSE Round(sum(ratingset1_.rating_num)/Count(ratingset1_.rating_num))
END)>=0
方法中,greaterThanOrEqualTo()
而不是selectExpression
,则无法使用roundExpression
。这样做会生成编译时错误,指示Expression<Integer>
和Expression<Object>
之间的类型不匹配。
那么如何在case...when
子句中使用与having
子句中相同的select
结构?
我还尝试删除类似Object
的表达式的泛型类型参数Expression selectExpression
,但这样做会导致NullPointerException
被抛出。 < / p>
此外,prodId
子句中给出的别名(rating
,select
)对生成的SQL没有影响,可以看出。为什么列没有别名?我错过了什么吗?
如果列是别名,则应该可以像下面那样编写having
子句。
having rating>=0
条件查询中的和having
应如下所示,
criteriaQuery.having(criteriaBuilder.greaterThanOrEqualTo(join.<Integer>get("rating"), 0));
但由于列在select
子句中没有别名,因此会引发异常。
java.lang.IllegalArgumentException: Unable to resolve attribute [rating] against path [null]
解决这种情况的方法是什么?无论如何,Group by
返回的行应该通过将null
替换为0
子句中case...when
生成的值列表中的select
来进行过滤。
我正在使用Hibernate 4.2.7 final提供的JPA 2.0。
修改
我尝试过以下表达式:
Expression<Integer> selectExpression = criteriaBuilder.<Integer>selectCase()
.when(quotExpression.isNull(), 0)
.<Integer>otherwise(roundExpression);
但它引发了以下异常:
Caused by: java.lang.NullPointerException
at java.lang.Class.isAssignableFrom(Native Method)
at org.hibernate.ejb.criteria.ValueHandlerFactory.isNumeric(ValueHandlerFactory.java:69)
at org.hibernate.ejb.criteria.predicate.ComparisonPredicate.<init>(ComparisonPredicate.java:69)
at org.hibernate.ejb.criteria.CriteriaBuilderImpl.greaterThanOrEqualTo(CriteriaBuilderImpl.java:468)
以下表达式如何起作用,
Expression<Integer> roundExpression = criteriaBuilder
.function("round", Integer.class, quotExpression);
两者都有相同的类型?
有没有办法将case...when
结构放在having
子句中?
修改
将表情类型更改为
Expression<Integer> selectExpression = criteriaBuilder
.<Integer>selectCase()
.when(quotExpression.isNull(), 0)
.<Integer>otherwise(roundExpression);
EclipseLink(2.3.2)中的因此可以在having
子句中使用。
如果是Hibernate提供程序,如果尝试更改NullPoiterExcpetion
(默认情况下返回selectCase()
)的表达式类型,则会抛出Expression<Object>
。
更新:
这个问题在Hibernate 5.0.5 final中仍然存在。
答案 0 :(得分:4)
这不太可能是Hibernate中的错误。在制定给出的标准查询时存在技术错误。以相同的例子,但以更简单的形式。
假设我们有兴趣生成以下SQL查询。
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
基于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)
此表rating
与另一个表product
有明显的多对一关系(prod_id
是引用{{prod_id
中的主键的外键1}}表)。
在这个问题中,我们只对product
子句中的CASE
构造感兴趣。
以下条件查询,
HAVING
按预期生成以下正确的SQL查询。
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));
}
从技术角度来看,请查看上述条件查询中的以下行。
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
问题中的类似内容如下所示。
criteriaQuery.having(criteriaBuilder.greaterThanOrEqualTo(selectExpression, criteriaBuilder.literal(1)));
在问题中看到原始表达式做同样的事情:
createQuery.having(criteriaBuilder.greaterThanOrEqualTo(selectExpression, 1));
尝试将此表达式传递给Expression<Integer> selectExpression = criteriaBuilder.<Integer>selectCase()
.when(quotExpression.isNull(), 0)
.<Integer>otherwise(roundExpression);
,如下所示。
criteriaBuilder.greaterThanOrEqualTo()
特别注意上面criteriaQuery.having(criteriaBuilder.greaterThanOrEqualTo(selectExpression, 0));
的第二个参数。它是greaterThanOrEqualTo()
。它应该是0
,而不是问题中提到的例外。
因此,在使用criteriaBuilder.literal(0)
构造中的表达式时,始终坚持在必要时使用CriteriaBuilder#literal(T value)
作为文字值,如上所述。
在Hibernate 4.3.6最终测试中,Hibernate 5.0.5最后选择。 稍后我将尝试在EclipseLink(最后的2.6.1)上运行相同的查询。不应该有怪癖。
对于查询的修改版本,EclipseLink完全没有问题,除非它需要构造函数参数(形参)的CriteriaBuilder#selectCase()
类型参数,如果使用构造函数表达式代替{{ 1}}这个问题毕竟与此无关。这仍然是EclipseLink中一个长期存在的错误 - an analogous example。