在JPA条件查询的'having'子句中使用'case ... when ... then ... else ... end'构造

时间:2013-11-20 06:13:10

标签: hibernate jpa criteria criteria-api having

以下条件查询计算不同产品组的评级平均值。

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子句中给出的别名(ratingselect)对生成的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中仍然存在。

1 个答案:

答案 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