我正试图使用JPA的Criteria 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
对我来说,这是由于整个表达式被放到了查询的 "group by "部分,而不仅仅是一个别名造成的。现在,我试着给表达式分配一个别名(它返回的是 Selection<T>
和groupBy接受表达式,因此,我只能真正在 multiselect
),但这并不影响查询的执行方式--没有任何改变。
如何使用Criteria API实现上述的按年月分组?也许除了使用 to_char
? 也许有一种方法可以给这个名字起个别名 groupBy
方法,从而导致它通过别名而不是整个表达式进行分组?
我认为这是PostgreSQL中的一个错误(错误来自那里,而不是Hibernate)。我试过用EclipseLink+Derby对你的代码稍加修改,就能完美地工作了。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
.