如何使用 JPA Criteria API / Hibernate 按 Case 语句分组

Posted

技术标签:

【中文标题】如何使用 JPA Criteria API / Hibernate 按 Case 语句分组【英文标题】:How to group by Case statement using JPA Criteria API / Hibernate 【发布时间】:2018-02-06 00:30:48 【问题描述】:

我正在尝试执行如下查询,通过 case 语句进行选择并按相同的 case 语句进行分组..

Select USER, 
  (CASE
    WHEN value between 0 AND 2 then '0-2'
    WHEN value between 3 AND 4 then '3-4'
    ELSE '5+'
  END) as CASE_STATEMENT ,
SUM(value)
.....
Group by user, CASE_STATEMENT

使用 JPA 2.0 Criteria API 和 Hibernate。

我的测试用例看起来像...

    CriteriaBuilder cb = em.getCriteriaBuilder()
    CriteriaQuery cq = cb.createQuery(Tuple)
    def root = cq.from(TestEntity)
    def userGet = root.get('user')
    def valueGet = root.get('value')
    def caseExpr =
            cb.selectCase()
                .when(cb.between(valueGet, 0, 2), '0-2')
                .when(cb.between(valueGet, 3, 4), '3-4')
                .otherwise('5+')
    def sumExpr = cb.sum(valueGet)

    cq.multiselect([userGet, caseExpr, sumExpr])
    cq.groupBy([userGet, caseExpr])
    log(typedQuery.unwrap(Query).queryString)
    List<Tuple> tuples = typedQuery.getResultList()

queryString 的日志语句读取

SELECT generatedAlias0.USER, 
   CASE 
     WHEN generatedAlias0.value BETWEEN 0 AND 2 THEN Cast(:param0 AS STRING) 
     WHEN generatedAlias0.value BETWEEN 3 AND 4 THEN Cast(:param1 AS STRING) 
     ELSE Cast(:param2 AS STRING) 
   END, 
   Sum(generatedAlias0.value) 
FROM   test AS generatedAlias0 
GROUP  BY generatedAlias0.USER, 
      CASE 
        WHEN generatedAlias0.value BETWEEN 0 AND 2 THEN Cast( 
        :param3 AS STRING) 
        WHEN generatedAlias0.value BETWEEN 3 AND 4 THEN Cast( 
        :param4 AS STRING) 
        ELSE Cast(:param5 AS STRING) 
      END 

在调用 typedQuery.getResultList() 时,我得到以下错误语句

javax.persistence.PersistenceException: org.hibernate.exception.GenericJDBCException: could not extract ResultSet

Caused by: org.h2.jdbc.JdbcSQLException: Column "TESTENTITY0_.VALUE" must be in the GROUP BY list; SQL statement:

select testentity0_.user as col_0_0_, case when testentity0_.value between 0 and 2 then cast(? as varchar(255)) when testentity0_.value between 3 and 4 then cast(? as varchar(255)) else cast(? as varchar(255)) end as col_1_0_, sum(testentity0_.value) as col_2_0_ from test testentity0_ group by testentity0_.user , case when testentity0_.value between 0 and 2 then cast(? as varchar(255)) when testentity0_.value between 3 and 4 then cast(? as varchar(255)) else cast(? as varchar(255)) end [90016-194]

我尝试按表达式分组的方式有问题吗?我还尝试按别名和数字文字 (1, 2) 进行分组

有没有其他方法可以构建 SQL 以获得相同的结果?

谢谢。

【问题讨论】:

现在看到同样的问题,找不到合适的解决方案。在问题中添加了一个赏金,希望有人会调查它并可以解释为什么案例中的列需要在 GROUP BY 中(因此,完全消除了计数的目的)。 直接在 H2 中运行完全相同的查询似乎成功了......所以它可能是一个 JPA 错误? 确定你在 SQL 中的问题,因为你有错误:org.h2.jdbc.JdbcSQLException: Column "TESTENTITY0_.VALUE" must be in the GROUP BY list 我相信这是因为您对案例语句进行了参数化。您可以尝试使用子查询吗? select user, case_statement, sum(value) as sum_value from ( select user, (case when value between 0 and 2 then '0-2' when value between 3 and 4 then '3-4' else '5+' end) as case_statement , value from test ) as t1 group by user, case_statement 作为@daggett 建议的替代方案,请尝试使用cb.literal('0-2')cb.literal('3-4')cb.literal('5+') 代替 【参考方案1】:

正如异常消息所暗示的,该问题与 DBMS 级别的 Group By 语句有关。见:https://www.percona.com/blog/2019/05/13/solve-query-failures-regarding-only_full_group_by-sql-mode/

要解决错误,您必须

将底层DBMS的Group By Mode设置为限制较少的级别(mysql允许disable only-full-group-by,但H2 does not(您可以尝试在jdbc连接字符串中设置MODE=MYSQL

(更好)

将属于 select 语句的所有列添加到 GROUP BY 语句或聚合函数,如 above 所述。

您应该能够构建一个满足 GROUP BY RESTRICTIONS 的嵌套查询。

为了救援,有一些(可能是 DBMS 特定的)聚合 函数(至少在MySQL 中)。为了欺骗 JPA 和 Hibernate 来理解这些,有 实现这一目标的几种方法,如在 https://vladmihalcea.com/hibernate-sql-function-jpql-criteria-api-query/ 和 https://vladmihalcea.com/the-jpa-entitymanager-createnativequery-is-a-magic-wand/

编辑

对比和补充上面的陈述,下面讨论后的发现是:

org.h2.expression.ExpressionColumn 类中的 h2 驱动程序在验证查询语法时引发了异常 解决方案需要在查询中设置和引用别名(在 case 语句子查询),目前在 Criteria API 中是不可能的 em>(见column aliases usually can't be referenced in the query itself) 一种解决方法是像这样创建 NativeQuery:
List<Tuple> tuples = em.createNativeQuery(
"SELECT generatedAlias0.USER, " +
"   CASE " +
"     WHEN generatedAlias0.value BETWEEN 0 AND 2 THEN Cast(:param0 AS VARCHAR) " +
"     WHEN generatedAlias0.value BETWEEN 3 AND 4 THEN Cast(:param1 AS VARCHAR) " +
"     ELSE Cast(:param2 AS VARCHAR) " +
"   END c, " +
"   Sum(generatedAlias0.value) as sumvalue " +
"FROM test AS generatedAlias0 " +
"GROUP  BY generatedAlias0.USER, c "
)
.setParameter("param0", "0-2")
.setParameter("param1", "3-4")
.setParameter("param2", "5+")
.getResultList();

【讨论】:

感谢您的建议。你能解释一下为什么查询在 H2 中使用相同的数据库连接设置可以正常工作吗?我没有问题按 SELECT 语句中的语句进行分组,而不必在我的组中声明该给定语句的所有属性.. @Frame91 确实,这种行为看起来很奇怪。在我看来,它不应该在任何情况下工作,除了宽容的 MySQL 系统。是否有可能在应用程序启动期间覆盖连接设置,例如 SQL 模式?您是否在 loglevel 调试时检查了日志? @Frame91 是的,我明白了。对于给定的示例,我发现 org.h2.expression.ExpressionColumn 类中的 h2 驱动程序 引发了异常,该类正在检查上述按限制分组。我还检查了 h2 控制台,它显示 h2 db 可以容忍这种违规行为。综上所述,为了实现兼容性,您必须为 case 语句设置别名或创建子查询,如您提到的问题中所述。 标准 api 不支持 FROM 语句中的子查询...我试图给 case 语句起别名,但它会导致相同的异常 @Frame91 可以使用getSelection() 进行子查询,如here 所述。我已经对此进行了测试,但不幸的是,导致了“异常分组”。为 subselect 语句分配和使用别名也不起作用。所以,就目前而言,我建议完全不要使用 Criteria API,并像 Vlad Mihalcea 所描述的那样构建一个NativeQuery。这只是关于 Criteria api 的一种解决方法,但至少是一个可行的解决方案。

以上是关于如何使用 JPA Criteria API / Hibernate 按 Case 语句分组的主要内容,如果未能解决你的问题,请参考以下文章

如何在 JPA Criteria API 上正确使用 JOIN

如何使用 JPA Criteria API / Hibernate 按 Case 语句分组

如何在JPA Criteria查询API中编写自定义查询作为根?

使用 JPA 的 Criteria API 按日期间隔分组

JPA Criteria Query API 和两列排序

比较 JPA Criteria API 中的日期实体