使用 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-05
和2020-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<T>
并且 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 时间戳