在今天的工作中,有一个分组查询需要按照日期分组统计业务数据。其中有个棘手的问题是业务时间是按照Calendar类型存的,如果是string类型的话,就可以直接截取年-月-日,然后按年-月-日group by就OK了。但是现在,涉及到时间的转换。想了半天,发现CriteriaBuilder里有个function方法,或许可以解决。网上翻了翻资料,没找到很合适的案例,不断尝试了下,问题解决了,现结合一个小例子跟大家分享下。
首先看数据:
一张订单表,有三个字段:订单ID,金额,支付时间,其中time字段在Entity里定义的是Calendar类型的。
需求:一段时间内按日期分组统计订单笔数和金额汇总数。
如果用原生sql,那么非常简单,如下:
一个sql就搞定了,但是现在用spring data JPA,就需要研究下了。关键点是将时间转换为string类型,然后截取yyyy-mm-dd,按照其分组统计就可以了。
废话不多说,上代码:
public void queryOrder(OrderParam orderParam) {
Calendar start = orderParam.getStart();
Calendar end = orderParam.getEnd();
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
//OrderSum指定了查询结果返回至自定义对象
CriteriaQuery query = cb.createQuery(OrderSum.class);
Root root = query.from(OrderEntity.class);
Path timePath = root.get("time");
Path feePath = root.get("fee");
List predicateList = new ArrayList();
if (start != null) {
predicateList.add(cb.greaterThanOrEqualTo(timePath, start));
}
if (end != null) {
end.add(Calendar.DATE, 1);
predicateList.add(cb.lessThan(timePath, end));
}
Predicate[] predicates = new Predicate[predicateList.size()];
predicates = predicateList.toArray(predicates);
//加上where条件
query.where(predicates);
//指定查询项,select后面的东西
Expression timeStr = cb.function("DATE_FORMAT", String.class, timePath, cb.parameter(String.class, "formatStr"));
query.multiselect(timeStr, cb.count(root).as(Integer.class), cb.sum(feePath));
query.groupBy(timeStr);
query.orderBy(cb.asc(timeStr));
TypedQuery typedQuery = entityManager.createQuery(query);
typedQuery.setParameter("formatStr", "%Y-%m-%d");
List result = typedQuery.getResultList();
for (OrderSum orderSum : result) {
//打印查询结果
System.out.println(orderSum.toString());
}
}
其中OrderParam是定义的一个查询条件类,其结构如下:
public class OrderParam {
private Calendar start;
private Calendar end;
public Calendar getStart() {
return start;
}
public void setStart(Calendar start) {
this.start = start;
}
public Calendar getEnd() {
return end;
}
public void setEnd(Calendar end) {
this.end = end;
}
}
OrderSum是自定义的一个查询结果返回实体,其结构如下:
public class OrderSum {
private String date;
private Integer count;
private Long totalFee;
//需要有个构造方法
public OrderSum(String date, Integer count, Long totalFee) {
this.date = date;
this.count = count;
this.totalFee = totalFee;
}
//此处省略了get、set、toString方法。
}
重点说明:
Expression
Hibernate:
select
date_format(orderentit0_.time,
?) as col_0_0_,
cast(count(orderentit0_.id) as signed) as col_1_0_,
sum(orderentit0_.fee) as col_2_0_
from
t_order orderentit0_
where
orderentit0_.time>=?
and orderentit0_.time
group by
date_format(orderentit0_.time,
?)
order by
date_format(orderentit0_.time,
?) asc
OrderSum{date='2018-08-18', count=1, totalFee=4}
OrderSum{date='2018-08-19', count=5, totalFee=18}
OrderSum{date='2018-08-20', count=4, totalFee=26}
OrderSum{date='2018-08-21', count=5, totalFee=13}
OrderSum{date='2018-08-22', count=5, totalFee=35}
OrderSum{date='2018-08-23', count=3, totalFee=9}
通过打印出来的sql,我们发现,第一个?号赋值上'%Y-%m-%d'后,就达到了我们想要的 date_format(orderentit0_.time,'%Y-%m-%d')效果。
后来,发现另一种方法也可以实现,貌似更简单:
public void queryOrder(OrderParam orderParam) {
Calendar start = orderParam.getStart();
Calendar end = orderParam.getEnd();
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
//OrderSum指定了查询结果返回至自定义对象
CriteriaQuery query = cb.createQuery(OrderSum.class);
Root root = query.from(OrderEntity.class);
Path timePath = root.get("time");
Path feePath = root.get("fee");
List predicateList = new ArrayList();
if (start != null) {
predicateList.add(cb.greaterThanOrEqualTo(timePath, start));
}
if (end != null) {
end.add(Calendar.DATE, 1);
predicateList.add(cb.lessThan(timePath, end));
}
Predicate[] predicates = new Predicate[predicateList.size()];
predicates = predicateList.toArray(predicates);
//加上where条件
query.where(predicates);
//指定查询项,select后面的东西
query.multiselect(cb.substring(timePath.as(String.class), 1, 10), cb.count(root).as(Integer.class), cb.sum(feePath));
query.groupBy(cb.substring(timePath.as(String.class), 1, 10));
query.orderBy(cb.asc(cb.substring(timePath.as(String.class), 1, 10)));
TypedQuery typedQuery = entityManager.createQuery(query);
List result = typedQuery.getResultList();
for (OrderSum orderSum : result) {
//打印查询结果
System.out.println(orderSum.toString());
}
}
cb.substring(timePath.as(String.class),直接转成string类型,然后截取年-月-日。执行结果如下:
Hibernate:
select
substring(cast(orderentit0_.time as char),
1,
10) as col_0_0_,
cast(count(orderentit0_.id) as signed) as col_1_0_,
sum(orderentit0_.fee) as col_2_0_
from
t_order orderentit0_
where
orderentit0_.time>=?
and orderentit0_.time
group by
substring(cast(orderentit0_.time as char),
1,
10)
order by
substring(cast(orderentit0_.time as char),
1,
10) asc
OrderSum{date='2018-08-18', count=1, totalFee=4}
OrderSum{date='2018-08-19', count=5, totalFee=18}
OrderSum{date='2018-08-20', count=4, totalFee=26}
OrderSum{date='2018-08-21', count=5, totalFee=13}
OrderSum{date='2018-08-22', count=5, totalFee=35}
OrderSum{date='2018-08-23', count=3, totalFee=9}
结果一样一样的, 通过打印出来的sql发现:substring(cast(orderentit0_.time as char),1,10),直接用cast转成char类型,然后截取了。
总结,其实第一种方式通过构建function适用性更广,如根据其他需求,可以使用DAYOFWEEK(),DAYOFMONTH()等多种函数,只要需要,就可构建。文章有不足之处,敬请斧正。