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里找到联系方式索要代码。

需注意,该封装,是针对于单表用的,并没有对多表联合查询做封装,因为我从来只有单表操作,从不做任何外键以及多表级联查询。

 

 

 

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐