简单封装JPA工具解决神烦的查询问题

757 阅读4分钟

JPA 痛点

我个人觉得 Spring Data JPA 基于注解的查询方式确实很好用,但是对于那种动态条件的查询就不怎么友好了,MyBatis 还可以使用 if 标签。(不知道是不是我不会用)
比如:后台的这个查询,查询条件可以输入也可以不输入,这样使用JPA 基于注解和接口方法的方式感觉就不怎么好用了。

image.png

那么一般会有如下的查询(当然查询方式很多,我只列举一种) image.png 如果搜索条件更多的话,这个查询还会更复杂,所以这一大坨的东西就值得优化一下了。

优化思路

其实可以看到上面的检索条件很类似,都是通过下面的方式增加条件过滤的。

Predicate equalPredate = criteriaBuilder.equal(root.get("userId").as(Long.class), request.getUserId());
predicates.add(equalPredate);

变化的只是 查询的字段名称,字段类型,以及查询方式(是模糊查询还是等值查询又或者是比较查询等等),那么我觉得改造的关键就是 把变化的抽出来,不变的地方做成模板

那么变化的是字段名称、类型以及查询方式,字段名称和类型可以从查询入参对象入手,可以通过反射获取到入参对象的字段名称和类型,这样就解决了过滤的条件了,那么查询方式可以通过自定义一个注解携带查询方式信息。

实际实现

自定义注解

package groot.user.behavior.support;

import java.lang.annotation.*;

/**
 * @Classname JpaParam
 * @Description
 * @Date 2021/8/12 15:54
 * @Created by wangchangjiu
 */
@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface JpaParam {

    QueryType queryType() default QueryType.EQUAL;

    enum QueryType {

        IN("in"),
        LT("lt"),
        LE("le"),
        GE("ge"),
        GT("gt"),
        NOT_LIKE("notLike"),
        NOTEQUAL("notEqual"),
        EQUAL("equal"),
        LIKE("like");

        private String queryType;

        QueryType(String queryType){
            this.queryType = queryType;
        }

        public String getQueryType() {
            return queryType;
        }

        public void setQueryType(String queryType) {
            this.queryType = queryType;
        }
    }
}

这个 JpaParam 注解有个属性 QueryType ,这个枚举定义了很多查询的类型,比如 “equal” 、“like” 等等。用户指定查询的方式

Repository 工具类

package groot.user.behavior.support;

import org.apache.commons.lang3.StringUtils;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.util.ReflectionUtils;

import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.Path;
import javax.persistence.criteria.Predicate;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.Objects;
import java.util.function.Consumer;

/**
 * @Classname RepositoryHelper
 * @Description
 * @Date 2021/8/12 16:27
 * @Created by wangchangjiu
 */
public class RepositoryHelper {

    /**
     * 带参数查询所有
     *
     * @param executor
     * @param paramObj
     * @param pageable
     * @param <T>
     * @return
     */
    public static <T> Page<T> findAll(JpaSpecificationExecutor<T> executor, Object paramObj, Pageable pageable) {
        Page<T> page = executor.findAll((root, criteriaQuery, criteriaBuilder) -> {
            Predicate predicate = criteriaBuilder.and();
            ReflectionUtils.doWithFields(paramObj.getClass(), field -> {
                JpaParam jpaParam = AnnotationUtils.getAnnotation(field, JpaParam.class);
                field.setAccessible(true);
                Object paramValue = field.get(paramObj);

                Class paramClazz = field.getType();
                if (jpaParam != null && Objects.nonNull(paramValue)) {
                    if (paramValue instanceof String) {
                        if (StringUtils.isBlank((String) paramValue)) {
                            return;
                        }
                    }

                    if (jpaParam.queryType() == JpaParam.QueryType.EQUAL) {
                        // 等于
                        predicate.getExpressions().add(criteriaBuilder.equal(root.get(field.getName()).as(paramClazz), paramValue));
                    } else if (jpaParam.queryType() == JpaParam.QueryType.LIKE) {
                        // 模糊查询
                        predicate.getExpressions().add(criteriaBuilder.like(root.get(field.getName()).as(paramClazz), "%" + paramValue + "%"));
                    } else if (jpaParam.queryType() == JpaParam.QueryType.NOT_LIKE) {

                        predicate.getExpressions().add(criteriaBuilder.notLike(root.get(field.getName()).as(paramClazz), "%" + paramValue + "%"));
                    } else if (jpaParam.queryType() == JpaParam.QueryType.NOTEQUAL) {
                        // 不等于
                        predicate.getExpressions().add(criteriaBuilder.notEqual(root.get(field.getName()).as(paramClazz), paramValue));
                    } else if (jpaParam.queryType() == JpaParam.QueryType.GT) {
                        if (!(paramValue instanceof Number)) {
                            throw new IllegalArgumentException("param value not Number");
                        }
                        predicate.getExpressions().add(criteriaBuilder.gt(root.get(field.getName()).as(paramClazz), (Number) paramValue));
                    } else if (jpaParam.queryType() == JpaParam.QueryType.GE) {
                        if (paramValue instanceof Date) {
                            predicate.getExpressions().add(criteriaBuilder.greaterThanOrEqualTo(root.get("createTime"), (Date) paramValue));
                        } else if (!(paramValue instanceof Number)) {
                            throw new IllegalArgumentException("param value not Number");
                        } else {
                            predicate.getExpressions().add(criteriaBuilder.ge(root.get(field.getName()).as(paramClazz), (Number) paramValue));
                        }
                    } else if (jpaParam.queryType() == JpaParam.QueryType.LT) {
                        if (!(paramValue instanceof Number)) {
                            throw new IllegalArgumentException("param value not Number");
                        }
                        predicate.getExpressions().add(criteriaBuilder.lt(root.get(field.getName()).as(paramClazz), (Number) paramValue));
                    } else if (jpaParam.queryType() == JpaParam.QueryType.LE) {
                        if (paramValue instanceof Date) {
                            predicate.getExpressions().add(criteriaBuilder.lessThanOrEqualTo(root.get(field.getName()).as(paramClazz), (Date) paramValue));
                        } else if (!(paramValue instanceof Number)) {
                            throw new IllegalArgumentException("param value not Number");
                        } else {
                            predicate.getExpressions().add(criteriaBuilder.le(root.get(field.getName()).as(paramClazz), (Number) paramValue));
                        }
                    } else if (jpaParam.queryType() == JpaParam.QueryType.IN) {
                        Path<Object> path = root.get(field.getName());
                        CriteriaBuilder.In<Object> in = criteriaBuilder.in(path);
                        if (paramClazz == String.class && String.valueOf(paramValue).contains(",")) {
                            Arrays.asList(String.valueOf(paramValue).split(",")).stream().forEach(item -> in.value(item));
                        } else if (Collection.class.isAssignableFrom(paramClazz) && paramValue instanceof Collection) {
                            for (Object item : Collection.class.cast(paramValue)) {
                                in.value(item);
                            }
                        }
                        predicate.getExpressions().add(criteriaBuilder.and(in));
                    }
                }
            });
            return predicate;
        }, pageable);
        return page;
    }

}

这样整个工具类就完成了,具体的代码可以看上面,就不做过多解释了,主要就是 利用反射获取参数字段的类型、参数名称、参数值,还有就是参数字段上的自定义注解 JpaParam,通过这个自定义注解来选择哪种查询方式。 我这里只封装了分页按条件查询数据的情况,其他的没有封装,因为按照这个思路其他的封装起来也是很容易的。

如何使用工具类

上面工具类和优化思路都已经介绍了,接下来看看怎么使用这个工具类来简化开发吧。

案例:我们对上面JPA痛点给出的案例进行改造。

定义查询对象,给查询对象字段增加查询方式

image.png 默认查询方式是等值查询。如果字段不需要查询,那就不加 JpaParam 这个注解。

查询业务代码

image.png 真正写查询的代码只有这两句,注意,这里传入的 repository 是 一个 JpaRepository,JpaSpecificationExecutor的接口,例如: image.png

怎么样?是不是感觉整个业务代码都干净了,不再是每个查询方法里面都是一大坨查询条件了。这里只是简单的封装一下,因为我们这种情况够用了。