SpringBoot中基于JPA的多条件动态查询封装

最近地摊经济盛行了,对于wshanshi这个吃货来说是再幸福不过的了。逛吃,逛吃,逛吃…怕是开心的要飞起来了呢!
要不咱也摆摊卖点啥去?看了看镜子里帅气的面孔,笑容愈发…
“老板,来一份wshanshi。酸甜,微辣,微辣…”
“好嘞!您稍等!我这就去把wshanshi叫来!”

在这里插入图片描述
“小二,来说一下SpringBoot中基于JPA的多条件动态查询封装。讲不好不但不给饭钱,还要把你们wshanshi小姐姐赔给我!”
“好嘞客官,给您免单!赶紧把她带走,带走!把我吃穷了都!”

用过SpringDataJPA的都知道,自定义接口继承JpaRepository<T,ID>接口、JpaSpecificationExecutor 接口之后,可以按照一定的规则定义接口方法名,实现变着花样的增删改查。

源码如下:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
原理在于:自定义接口继承了JpaRepository<T, ID>和JpaSpecificationExecutor 接口。由于JpaRepository<T, ID> 继承了 PagingAndSortingRepository<T, ID>接口和QueryByExampleExecutor接口,且PagingAndSortingRepository<T, ID>又继承了CrudRepository<T, ID>接口。因此可以直接使用默认封装的增删改查方法、分页排序方法、多条件动态查询方法。

当然你也可以自定义,像常见的根据用户名查找用户、根据用户编号删除用户等等。
在这里插入图片描述
有兴趣的可以去看一下源码,这里就不多说了。开始今天的主题:SpringBoot中基于JPA的多条件动态查询封装。
在这里插入图片描述
JPA中基于Specification的多条件动态查询,Specification的使用点这里:SpringDataJPA中使用Specification进行表连接多条件分页动态查询

像下图这样,使用Specification时,如果不进行二次封装,那么一个项目中可能会写多个动态条件查询方法。整体看起来代码是冗余的。
这个图片中的代码,虽然用法对,写法也对,但是如果出现过多,不用想绝对会被Diss。别问我怎么知道的,不信你尝试。到时候一定要控制住你的右手,离自己的小脸远一点,力度小一点。哇哈哈哈哈哈…
在这里插入图片描述
那么,为了避免冲动起来自己扇自己,有没有更好的办法,可以看起来既清爽简洁,使用起来又方便快捷的?

当然,我们可以选择封装一下。自定义注解实现多条件动态查询(可表关联查询),使用方便简洁,清晰明了。何乐而不为呢?
在这里插入图片描述
具体操作如下:

一、自定义注解查询类
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Query {

    /**
     * 基本对象的属性名
     */
    String propName() default "";

    /**
     * 基本对象的属性名LIST OR
     */
    String[] orPropNames() default {};

    String orPropVal() default "";

    /**
     * 查询方式 默认精确查询
     */
    Type type() default Type.EQUAL;

    /**
     * 多表连接查询的属性名,如User类中的role
     *
     * @return
     */
    String joinName() default "";

    /**
     * 左连接
     *
     * @return
     */
    Join join() default Join.LEFT;

    /**
     * 多字段模糊搜索,仅支持String类型字段,多个用逗号隔开, 如用户名和手机号模糊查询@Query(blurry = "username,phone")
     *
     * @return
     */
    String blurry() default "";

    enum Type {
        /**
         * 相等,精确查询
         */
        EQUAL,
        /**
         * 大于等于
         */
        GREATER_THAN,
        /**
         * 小于等于
         */
        LESS_THAN,
        /**
         * 左右均模糊查询:%a%
         */
        INNER_LIKE,
        /**
         * 左模糊查询:%a
         */
        LEFT_LIKE,
        /**
         * 右模糊查询: a%
         */
        RIGHT_LIKE,
        /**
         * 小于
         */
        LESS_THAN_NQ,
        /**
         * 在...范围内
         */
        IN,
        /**
         * 或者
         */
        OR
    }

    enum Join {
        /**
         * 左连接
         */
        LEFT,
        /**
         * 右连接
         */
        RIGHT,
    }
}
二、定义Predicate转换类(该类可Copy直接使用)

该类内部通过反射获取多条件查询属性,进行动态拼接。实现了多条件 精确查询、模糊查询、范围查询,关联查询。基本操作是够用了。

@Slf4j
public class QueryUtil {
    @SuppressWarnings("unchecked")
    public static <R, Q> Predicate getPredicate(Root<R> root, Q query, CriteriaBuilder cb) {
        List<Predicate> list = new ArrayList<>();

        if (query == null) {
            return cb.and(list.toArray(new Predicate[list.size()]));
        }
        try {
            List<Field> fields = getAllFields(query.getClass(), new ArrayList<>());
            for (Field field : fields) {
                boolean accessible = field.isAccessible();
                field.setAccessible(true);
                Query q = field.getAnnotation(Query.class);
                if (q != null) {
                    String propName = q.propName();
                    String joinName = q.joinName();
                    String blurry = q.blurry();
                    String attributeName = isBlank(propName) ? field.getName() : propName;
                    Class<?> fieldType = field.getType();
                    Object val = field.get(query);
                    if (ObjectUtil.isNull(val) || "".equals(val)) {
                        continue;
                    }
                    Join join = null;
                    // 模糊多字段
                    if (ObjectUtil.isNotEmpty(blurry)) {
                        String[] blurrys = blurry.split(",");
                        List<Predicate> orPredicate = new ArrayList<>();
                        for (String s : blurrys) {
                            orPredicate.add(cb.like(root.get(s)
                                    .as(String.class), "%" + val.toString() + "%"));
                        }
                        Predicate[] p = new Predicate[orPredicate.size()];
                        list.add(cb.or(orPredicate.toArray(p)));
                        continue;
                    }
                    if (ObjectUtil.isNotEmpty(joinName)) {
                        String[] joinNames = joinName.split(">");
                        for (String name : joinNames) {
                            switch (q.join()) {
                                case LEFT:
                                    if (ObjectUtil.isNotEmpty(join)) {
                                        join = join.join(name, JoinType.LEFT);
                                    } else {
                                        join = root.join(name, JoinType.LEFT);
                                    }
                                    break;
                                case RIGHT:
                                    if (ObjectUtil.isNotEmpty(join)) {
                                        join = join.join(name, JoinType.RIGHT);
                                    } else {
                                        join = root.join(name, JoinType.RIGHT);
                                    }
                                    break;
                            }
                        }
                    }
                    switch (q.type()) {
                        case EQUAL:
                            list.add(cb.equal(getExpression(attributeName, join, root)
                                    .as((Class<? extends Comparable>) fieldType), val));
                            break;
                        case GREATER_THAN:
                            list.add(cb.greaterThanOrEqualTo(getExpression(attributeName, join, root)
                                    .as((Class<? extends Comparable>) fieldType), (Comparable) val));
                            break;
                        case LESS_THAN:
                            list.add(cb.lessThanOrEqualTo(getExpression(attributeName, join, root)
                                    .as((Class<? extends Comparable>) fieldType), (Comparable) val));
                            break;
                        case LESS_THAN_NQ:
                            list.add(cb.lessThan(getExpression(attributeName, join, root)
                                    .as((Class<? extends Comparable>) fieldType), (Comparable) val));
                            break;
                        case INNER_LIKE:
                            list.add(cb.like(getExpression(attributeName, join, root)
                                    .as(String.class), "%" + val.toString() + "%"));
                            break;
                        case LEFT_LIKE:
                            list.add(cb.like(getExpression(attributeName, join, root)
                                    .as(String.class), "%" + val.toString()));
                            break;
                        case RIGHT_LIKE:
                            list.add(cb.like(getExpression(attributeName, join, root)
                                    .as(String.class), val.toString() + "%"));
                        case IN:
                            if (CollUtil.isNotEmpty((Collection<Long>) val)) {
                                list.add(getExpression(attributeName, join, root).in((Collection<Long>) val));
                            }
                            break;
                        case OR:
                            if (Boolean.valueOf(q.orPropVal())) {
                                Predicate[] pArrays = (Predicate[]) Arrays.asList(q.orPropNames()).stream().map(
                                        prop -> cb
                                                .equal(getExpression(prop, null, root).as(String.class),
                                                        q.orPropVal())).toArray();
                                cb.or(pArrays);
                            }
                            break;
                    }
                }
                field.setAccessible(accessible);
            }
        } catch (Exception e) {
            log.error(e.getMessage(), e);
        }
        return cb.and(list.toArray(new Predicate[list.size()]));
    }

    @SuppressWarnings("unchecked")
    private static <T, R> Expression<T> getExpression(String attributeName, Join join, Root<R> root) {
        if (ObjectUtil.isNotEmpty(join)) {
            return join.get(attributeName);
        } else {
            return root.get(attributeName);
        }
    }

    @SuppressWarnings("unchecked")
    public static boolean isBlank(final CharSequence cs) {
        int strLen;
        if (cs == null || (strLen = cs.length()) == 0) {
            return true;
        }
        for (int i = 0; i < strLen; i++) {
            if (Character.isWhitespace(cs.charAt(i)) == false) {
                return false;
            }
        }
        return true;
    }

    @SuppressWarnings("unchecked")
    private static List<Field> getAllFields(Class clazz, List<Field> fields) {
        if (clazz != null) {
            fields.addAll(Arrays.asList(clazz.getDeclaredFields()));
            getAllFields(clazz.getSuperclass(), fields);
        }
        return fields;
    }
}
三、自定义条件查询类UserQueryCriteria

需要什么查询条件在这里面加。

注意:propName是你实体类中对应的属性名,需和实体类属性一致。 如果propName省略不写,默认按照该查询类中你定义的属性名去查询。如果你既没定义propName,同时属性名又写错,就会出错了。

@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class UserQueryCriteria {
    // 精确查询
    @Query(propName = "id",type = Query.Type.EQUAL)//propName的值和实体类中属性值一致
    private Long id;

    // 左右模糊查询:%username%
    @Query(propName = "username",type = Query.Type.INNER_LIKE)//propName的值和实体类中属性值一致
    private String username;//实体类中用户名就叫做username

    // 精确查询
    @Query(type = Query.Type.EQUAL)
    private String email;

    // 关联查询 joinName是外键名 propName是属性名(必须与实体类中属性名对应)type是查询方式,此处是关联单属性查询
//    @Query(joinName = "", propName = "",type = Query.Type.EQUAL)

    // 范围查询,此处是关联单属性范围查询。如:查询id 在(1,2,3)中的对象
//    @Query(joinName = "", propName = "", type = Query.Type.IN)

    // 日期范围查询定义 type  GREATER_THAN  LESS_THAN就好

}
四、 UserService接口

可定义多条件分页查询,传入查询对象和分页Pageable对象。
在这里插入图片描述

五、 UserServiceImpl中方法实现
 @Override
    public Map<String, Object> queryAll(UserQueryCriteria criteria, Pageable pageable) {
        Page<User> page = userRepository.findAll((root, criteriaQuery, criteriaBuilder) -> QueryUtil
                .getPredicate(root, criteria, criteriaBuilder), pageable);
        Map map = PageUtil.toPage(page);
        return map;
    }

PageUtil

public class PageUtil extends cn.hutool.core.util.PageUtil {

    /**
     * List 分页
     *
     * @param page
     * @param size
     * @param list
     * @return
     */
    public static List toPage(int page, int size, List list) {
        int fromIndex = page * size;
        int toIndex = page * size + size;

        if (fromIndex > list.size()) {
            return new ArrayList();
        } else if (toIndex >= list.size()) {
            return list.subList(fromIndex, list.size());
        } else {
            return list.subList(fromIndex, toIndex);
        }
    }
    
    /**
     * list转page
     *
     * @param list
     * @param pageable
     * @param <T>
     * @return
     */
    public static  <T> Page<T> listConvertToPage(List<T> list, Pageable pageable) {
        int start = (int) pageable.getOffset();
        int end = (start + pageable.getPageSize()) > list.size() ? list.size() : (start + pageable.getPageSize());
        return new PageImpl<T>(list.subList(start, end), pageable, list.size());
    }

    /**
     * Page 数据处理,预防redis反序列化报错
     *
     * @param page
     * @return
     */
    public static Map toPage(Page page) {
        Map<String, Object> map = new LinkedHashMap<>(2);
        map.put("content", page.getContent());
        map.put("totalElements", page.getTotalElements());
        return map;
    }

    /**
     * @param object
     * @param totalElements
     * @return
     */
    public static Map toPage(Object object, Object totalElements) {
        Map<String, Object> map = new LinkedHashMap<>(2);
        map.put("content", object);
        map.put("totalElements", totalElements);

        return map;
    }
}
六、Controller中定义

查询时根据需求传入查询条件和分页信息。
在这里插入图片描述
到此就可以了。只需要在查询类中属性上方添加对应的查询注解,传入接口就好了。不用再写好长好长的代码,减少冗余。是不是happy多了。

“打烊了,打烊了!”
“客官慢走,别忘了把wshanshi带走哈!”

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章