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

Posted

技术标签:

【中文标题】使用 JPA 的 Criteria API 按日期间隔分组【英文标题】:Group by date intervals using JPA's Criteria API 【发布时间】:2020-09-11 00:58:08 【问题描述】:

我正在尝试使用 JPA 的 Criteria API 按日期间隔对实体进行分组。我使用这种查询实体的方式,因为这是服务 API 请求的服务的一部分,API 请求可能要求任何实体的任何字段,包括排序、过滤、分组和聚合。除了按日期字段分组外,一切正常。我的底层 DBMS 是 PostgreSQL。

举个简单的例子,这是我的实体类:

@Entity
@Table(name = "receipts")
public class DbReceipt 
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    private Date sellDate;
    // Many other fields

这个例子讨论了对我的“月”间隔进行分组(因此按年+月分组),但最后我正在寻找一种可以让我按任何间隔分组的解决方案,例如“年”、“日”或“分钟”。

我想要实现的是以下查询,但使用 Criteria API:

SELECT TO_CHAR(sell_date, 'YYYY-MM') AS alias1 FROM receipts GROUP BY alias1;

我的尝试是这样的:

@Service
public class ReceiptServiceImpl extends ReceiptService 
    @Autowired
    private EntityManager em;

    @Override
    public void test() 
        CriteriaBuilder cb = em.getCriteriaBuilder();
        CriteriaQuery<Object[]> query = cb.createQuery(Object[].class);
        Root<?> root = query.from(DbReceipt.class);
        Expression<?> expr = cb.function("to_char", String.class, root.get("sellDate"), cb.literal("YYYY-MM"));

        query.groupBy(expr);
        query.multiselect(expr);

        TypedQuery<Object[]> typedQuery = em.createQuery(query);
        List<Object[]> resultList = typedQuery.getResultList();
    

我使用to_char 函数而不是MONTH 和类似函数的原因是我需要不将2019-052020-05 之类的实体组合在一起。我还将这个例子缩小到只有年和月以保持简短,但目标是按任何日期间隔分组。

上面的代码创建了以下导致错误的查询(启用 SQL 日志记录):

Hibernate: select to_char(dbreceipt0_.sell_date, ?) as col_0_0_ from receipts dbreceipt0_ group by to_char(dbreceipt0_.sell_date, ?)
24-05-2020 12:16:30.071 [http-nio-1234-exec-5] WARN  o.h.e.jdbc.spi.SqlExceptionHelper.logExceptions - SQL Error: 0, SQLState: 42803
24-05-2020 12:16:30.071 [http-nio-1234-exec-5] ERROR o.h.e.jdbc.spi.SqlExceptionHelper.logExceptions - ERROR: column "dbreceipt0_.sell_date" must appear in the GROUP BY clause or be used in an aggregate function
  Position: 16

这对我来说是由于整个表达式被放入查询的“分组依据”部分,而不仅仅是一个别名。现在,我尝试为表达式分配一个别名(它返回 Selection&lt;T&gt; 并且 groupBy 接受表达式,因此我只能在 multiselect 中真正使用它),但这并不影响查询的执行方式 -没有任何改变。

如何使用 Criteria API 实现如上所述的按年和月分组?除了使用to_char之外,也许还有其他方法?也许有一种方法可以为 groupBy 方法提供别名,这会导致它按别名而不是整个表达式进行分组?

【问题讨论】:

【参考方案1】:

我认为这是 PostgreSQL 中的一个错误(错误来自那里,而不是来自 Hibernate)。我已经尝试使用 EclipseLink + Derby 对您的代码进行稍微修改的版本,并且效果很好。 请注意,我必须使用数字而不是字符串,因为 Derby DB 没有与 TO_CHAR 等效的函数。

Expression<Integer> year = cb.function("YEAR", Integer.class, root.get("sellDate"));
Expression<Integer> month = cb.function("MONTH", Integer.class, root.get("sellDate"));
Expression<Integer> expr = cb.sum(month, cb.prod(12, year));
query.groupBy(expr);
query.multiselect(expr);

这将返回以下 SQL:

SELECT (MONTH(MY_DATE) + (12 * YEAR(MY_DATE))) 
FROM MY_DATE_TABLE 
GROUP BY (MONTH(MY_DATE) + (12 * YEAR(MY_DATE)))

请注意,在 JPA 条件查询中没有可移植的解决方案来处理日期。如果要同时查询的组数不是太多,我会采用更实用的方法,您可以在 Java 中找到日期并将它们作为文字传递给查询构建器。

另一种解决方法是使用groupBy(root.get("sellDate")) 进行查询,然后根据所需的时间段在 Java 中聚合结果。

Post Scriptum:我认为这无关紧要,但是我将查询的返回类型从 Object[] 修改为 Object

【讨论】:

虽然这特别适用于年和月,但我的一般问题是我需要支持所有日期间隔,而不仅仅是问题中的年+月。遗憾的是,我没有看到如何将这种方法应用于“日”——因此还有所有更细化的间隔——由于月份的天数不同以及闰年——我无法给出一个单一的值在“product”函数中将“DAY”函数的结果乘以。我喜欢你的年+月案例的方法,但对于我更一般的案例来说,它似乎不够通用。 已编辑问题以更详细地解释我想要实现的目标 @JacekŚlimok 我想你没有明白我的意思。如果您切换到 EclipseLink 作为 JPA 提供程序,我确实认为您的方法非常有效。我使用整数方法只是因为我使用的是不同的数据库。我稍后会尝试使用 Hibernate 哦,好吧,现在我明白了。不过这很有趣,因为您的代码在我的情况下有效,而原始的 TO_CHAR 版本却没有。我已经在hibternate的bugtracker上发布了这个,我们看看会发生什么:hibernate.atlassian.net/browse/HHH-14045 @JacekŚlimok 请注意,两者之间的区别在于? 部分。查看编译后的查询,很明显 Hibernate 为两个 'YYYY-MM' 字符串创建了参数,而不是实际将它们视为文字。我敢打赌,在您的情况下,Postgres 根本无法识别来自SELECT? 和来自GROUP BY? 是相同的值,因此错误

以上是关于使用 JPA 的 Criteria API 按日期间隔分组的主要内容,如果未能解决你的问题,请参考以下文章

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

如何使用 JPA Criteria API 连接不相关的实体

JPA 如何获取 Criteria API 中使用的参数标记的数量?

使用 JPA2 Criteria API 选择 MAX 时间戳

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

JPA/Criteria API - Like & equal 问题