详解Jpa动态复杂条件查询,查询指定字段、并包括sum、count、avg等数学运算,包括groupBy分组
Jpa是我一直推荐在Springboot及微服务项目中使用的数据库框架,并由于官方的并不是十分友好和易用的api,导致很多人使用起来并不方便,下面就来展示一下我对api进行了封装后的代码。大大减轻了使用难度。效果展示首先我们直接来看最终的结果:譬如有个entity叫PtActivity,它有一个Repository。public interface PtActivityRepos...
Jpa是我一直推荐在Springboot及微服务项目中使用的数据库框架,并由于官方的并不是十分友好和易用的api,导致很多人使用起来并不方便,下面就来展示一下我对api进行了封装后的代码。大大减轻了使用难度。
效果展示
首先我们直接来看最终的结果:
譬如有个entity叫PtActivity,它有一个Repository。
public interface PtActivityRepository extends JpaRepository<PtActivity, Long>,
JpaSpecificationExecutor<PtActivity> {
}
继承了JpaSpecificationExecutor后,它拥有了这样一个方法:
Page<T> findAll(@Nullable Specification<T> var1, Pageable var2);
即传入一个Specification对象,即可完成条件查询,来看一个简单的例子。MySpecification就是封装好的工具类,能够大幅简化jpa构建条件查询的操作。
private Page<PtActivity> find(String states, String name, String begin, String end, Pageable pageable) {
MySpecification<PtActivity> mySpecification = new MySpecification<>();
String[] stateArray = states.split(",");
if (begin != null) {
mySpecification.add(Restrictions.gte("createTime", CommonUtil.beginOfDay(begin), true));
}
if (end != null) {
mySpecification.add(Restrictions.lte("createTime", CommonUtil.endOfDay(end), true));
}
mySpecification.add(Restrictions.in("state", Arrays.asList(stateArray), true));
mySpecification.add(Restrictions.like("name", name, true));
mySpecification.add(Restrictions.eq("deleteFlag", false, true));
return ptActivityManager.findAll(mySpecification, pageable);
}
该demo构建了一个查询createTime大于begin,小于end,并且state字段的值,在某个数组范围内,并且name字段like一个传来的值,并且deleteFlag字段等于false的查询条件。如果哪个字段没传值,就忽略该筛选条件。
这样代码看起来就很容易理解,下面看一个稍微复杂点的例子:
public void find() {
MySpecification<PtActivity> criteriaQueryBuilder = new MySpecification<>();
criteriaQueryBuilder.addAll(Restrictions.pickSome("id","state"));
//criteriaQueryBuilder.add(Restrictions.sum("id"));
//criteriaQueryBuilder.add(Restrictions.max("state"));
criteriaQueryBuilder.add(Restrictions.gte("createTime", CommonUtil.beginOfDay("2019-05-01"), true));
criteriaQueryBuilder.add(Restrictions.lte("createTime", CommonUtil.endOfDay("2019-05-31"), true));
//criteriaQueryBuilder.add(Restrictions.groupBy("state"));
List<Tuple> tuples = criteriaQueryBuilder.findResult(em, PtActivity.class);
for (Tuple tuple : tuples) {
Object count = tuple.get(0);
System.out.println(count);
}
}
该方法完成了只查询id、state字段,并且createTime在某个时间范围内的。如果把注释放开,就是查询sum(id),max(state) 并且groupBy state字段。
详细解析
何为Specification
还是回到Jpa的这个接口,可以看到,要完成一次查询,主要的工作就是构建Specification,而Specification接口中,主要就是一个方法即toPredicate方法。这个方法就是构建select * from table where xxxxx语句的where条件。其他的not、and都是对Specification的一些交集、并集,也就是where语句里的and、or。
public interface JpaSpecificationExecutor<T> {
Optional<T> findOne(@Nullable Specification<T> var1);
List<T> findAll(@Nullable Specification<T> var1);
Page<T> findAll(@Nullable Specification<T> var1, Pageable var2);
List<T> findAll(@Nullable Specification<T> var1, Sort var2);
long count(@Nullable Specification<T> var1);
}
public interface Specification<T> extends Serializable {
long serialVersionUID = 1L;
static <T> Specification<T> not(Specification<T> spec) {
return Specifications.negated(spec);
}
static <T> Specification<T> where(Specification<T> spec) {
return Specifications.where(spec);
}
default Specification<T> and(Specification<T> other) {
return Specifications.composed(this, other, CompositionType.AND);
}
default Specification<T> or(Specification<T> other) {
return Specifications.composed(this, other, CompositionType.OR);
}
@Nullable
Predicate toPredicate(Root<T> var1, CriteriaQuery<?> var2, CriteriaBuilder var3);
}
我们可以这样理解,要做的一切事情,就是为了构建Predicate对象,该对象组合了N多个查询子语句。
所以我们要做的就是根据前端传来的字段构建多个Predicate对象,再将这多个Predicate组装成一个Predicate对象,就完成了条件查询的构建。
如果采用官方api来完成一次复杂条件查询,代码可能是下面这样的:
public void findTemp() {
ptActivityManager.findAll(new Specification<PtActivity>() {
@Override
public Predicate toPredicate(Root<PtActivity> root, CriteriaQuery<?> criteriaQuery, CriteriaBuilder
criteriaBuilder) {
Path idPath = root.get("id");
Predicate predicate1 = criteriaBuilder.equal(idPath, "12345");
Path statePath = root.get("state");
Predicate predicate2 = criteriaBuilder.equal(statePath, "1");
return criteriaBuilder.and(predicate1, predicate2);
}
});
}
猛一看,其实还是挺乱的,什么root、criteriaQuery、criteriaBuilder都是些什么鬼,怎么组建的Predicate,新手一看,比较茫然。下面就来解惑一下,这些都是什么鬼。
解析原生的底层查询
事实上,要完成一次条件查询,它的流程是这样的:
public List<Tuple> findResult(EntityManager entityManager, Class<T> t) {
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
CriteriaQuery<Tuple> criteriaQuery = criteriaBuilder.createTupleQuery();
Root<T> root = criteriaQuery.from(t);
if (!selectorList.isEmpty()) {
criteriaQuery.multiselect(buildSelections(criteriaBuilder, root));
}
if (!criterionList.isEmpty()) {
criteriaQuery.groupBy(buildGroupBy(root));
}
criteriaQuery.where(toPredicate(root, criteriaQuery, criteriaBuilder));
return entityManager.createQuery(criteriaQuery).getResultList();
}
先获取EntityManager,然后从EntityManager中获取CriteriaBuilder,再从CriteriaBuilder中创建一个CriteriaQuery,然后将各个条件都组合到CriteriaQuery中,最终通过entityManager.createQuery(criteriaQuery).getResultList()来获取到查询结果。
譬如一次查询是这样的:select a, b, sum(c) from table where a > 0 and c < 1 group by a
那么a、b、sum(c)都属于CriteriaQuery中的select参数,where后面的条件都属于CriteriaQuery的where后的参数,groupBy和having都属于CriteriaQuery的对应的参数。最终组合成一个丰满的CriteriaQuery,并由EntityManager来createQuery并获取结果集。
可以看到里面有非常完整的构建的方法。我们要做的就是将select后面的组合成Selection对象,where后面的组合成Predicate对象,having、groupBy什么的按照属性类型组合即可。
这些Selection、Predicate对象怎么构建呢,就是靠CriteriaBuilder。
CriteriaBuilder里的箭头的方法,都是构建Selection的。
这几个都是构建Predicate的。
至于用来做having,groupBy的更简单,直接用root.get("字段名")就可以了。
知道了这些,问题就更简单了,我们要做的就是把构建这3个组合的方法给封装起来就好了。不然用上面官方提供的这些,很不方便。
JpaSpecificationExecutor怎么理解
我们知道,平时用这个findAll(Specification var1)时,只需要构建好Predicate即可。
里面的root,CriteriaQuery和builder都已经被Jpa赋值好了,我们只需要关注Predicate的构建,也就是说,这个findAll方法只能完成where条件的构建,而不能实现select后面属性的选择和groupBy的构建。
jpa怎么给root什么的赋值的呢,其实是这样的,Jpa是一种规范,Hibernate、OpenJPA对其进行了实现,譬如Springboot默认使用Hibernate实现Jpa,也就是上一小节提到的EntityManager那一套,Hibernate创建了CriteriaQuery和Builder和root,并且将值赋给上图的各参数中,供用户使用,来构建where条件需要的Predicate对象。
编码封装API
以上如果都理解了,那么就可以来编码了,我们做好构建Selection、Predicate、Expression的封装就可以了,就能完成所有的单表复杂查询。
定义一个终极接口:
/**
* 适用于对单表做sum、avg、count等运算时使用,并且查询条件不固定,需要动态生成predicate</p>
* 如select sum(a), count(b), count distinct(c) from table where a = ? & b = ?
*
* @author wuweifeng wrote on 2018/1/3.
*/
public interface CriteriaQueryBuilder<T> extends Specification<T> {
/**
* 构建select字段
*/
List<Selection<?>> buildSelections(CriteriaBuilder builder, Root<T> root);
/**
* 构建groupBy字段
*/
List<Expression<?>> buildGroupBy(Root<T> root);
/**
* 获取返回的结果集
*/
List<Tuple> findResult(EntityManager entityManager, Class<T> t);
}
只要完成了这4个(包括Specification里的toPredicate)方法,就能从findResult里得到你想要的结果集。
提供一个实现类:
package com.maimeng.jd.global.specify;
import com.maimeng.jd.global.specify.simple.IExpression;
import com.maimeng.jd.global.specify.simple.IPredicate;
import com.maimeng.jd.global.specify.simple.ISelector;
import javax.persistence.EntityManager;
import javax.persistence.Tuple;
import javax.persistence.criteria.*;
import java.util.ArrayList;
import java.util.List;
/**
* 定义一个查询条件容器,用于构建where条件、select字段、groupBy字段
*
* @author wuwf on 17/6/6.
*/
public class NbQueryBuilder<T> implements CriteriaQueryBuilder<T> {
private List<IPredicate> criterionList = new ArrayList<>();
private List<ISelector> selectorList = new ArrayList<>();
private List<IExpression> expressionList = new ArrayList<>();
@Override
public List<Selection<?>> buildSelections(CriteriaBuilder builder, Root<T> root) {
List<Selection<?>> selections = new ArrayList<>();
for (ISelector iSelector : selectorList) {
selections.add(iSelector.getSelection(root, builder));
}
return selections;
}
@Override
public List<Expression<?>> buildGroupBy(Root<T> root) {
List<Expression<?>> expressions = new ArrayList<>();
for (IExpression expression : expressionList) {
expressions.add(expression.getGroupBy(root));
}
return expressions;
}
@Override
public Predicate toPredicate(Root<T> root, CriteriaQuery<?> criteriaQuery, CriteriaBuilder criteriaBuilder) {
if (!criterionList.isEmpty()) {
List<Predicate> predicates = new ArrayList<>();
for (IPredicate c : criterionList) {
predicates.add(c.toPredicate(root, criteriaBuilder));
}
return criteriaBuilder.and(predicates.toArray(new Predicate[0]));
}
return criteriaBuilder.conjunction();
}
@Override
public List<Tuple> findResult(EntityManager entityManager, Class<T> t) {
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
CriteriaQuery<Tuple> criteriaQuery = criteriaBuilder.createTupleQuery();
Root<T> root = criteriaQuery.from(t);
if (!selectorList.isEmpty()) {
criteriaQuery.multiselect(buildSelections(criteriaBuilder, root));
}
if (!criterionList.isEmpty()) {
criteriaQuery.groupBy(buildGroupBy(root));
}
criteriaQuery.where(toPredicate(root, criteriaQuery, criteriaBuilder));
return entityManager.createQuery(criteriaQuery).getResultList();
}
/**
* 增加简单条件表达式
*/
public void add(ISelector iSelector) {
if (iSelector != null) {
selectorList.add(iSelector);
}
}
/**
* 增加where子语句
*/
public void add(IPredicate iPredicate) {
if (iPredicate != null) {
criterionList.add(iPredicate);
}
}
/**
* 增加尾部语句
*/
public void add(IExpression iExpression) {
if (iExpression != null) {
expressionList.add(iExpression);
}
}
public <R extends ISelector> void addAll(List<R> selectors) {
selectorList.addAll(selectors);
}
}
最终在service里使用起来就是这样的:
public void find() {
NbQueryBuilder<PtActivity> criteriaQueryBuilder = new NbQueryBuilder<>();
criteriaQueryBuilder.addAll(Restrictions.pickSome("id", "state"));
//criteriaQueryBuilder.add(Restrictions.sum("id"));
//criteriaQueryBuilder.add(Restrictions.max("state"));
criteriaQueryBuilder.add(Restrictions.gte("createTime", CommonUtil.beginOfDay("2019-05-01"), true));
criteriaQueryBuilder.add(Restrictions.lte("createTime", CommonUtil.endOfDay("2019-05-31"), true));
//criteriaQueryBuilder.add(Restrictions.groupBy("state"));
List<Tuple> tuples = criteriaQueryBuilder.findResult(em, PtActivity.class);
for (Tuple tuple : tuples) {
Object count = tuple.get(0);
System.out.println(count);
}
}
当然,如果你不需要构建Selection、groupBy时,也可以只构建Predicate,然后使用jpa的findAll()方法即可。
代码结构如下,都是一些对构建条件的封装和一个Restrictions的工厂类。
由于代码很多,我就不一一贴了。能理解全文的,自己应该也能写出来。
写不出来的,可以去我的开源区块链平台项目https://gitee.com/tianyalei/md_blockchain里找到联系方式索要代码。
需注意,该封装,是针对于单表用的,并没有对多表联合查询做封装,因为我从来只有单表操作,从不做任何外键以及多表级联查询。
更多推荐
所有评论(0)