欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

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

程序员文章站 2022-04-16 08:15:36
...

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

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

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

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

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

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

当然,我们可以选择封装一下。自定义注解实现多条件动态查询(可表关联查询),使用方便简洁,清晰明了。何乐而不为呢?
SpringBoot中基于JPA的多条件动态查询封装
具体操作如下:

一、自定义注解查询类
@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对象。
SpringBoot中基于JPA的多条件动态查询封装

五、 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中定义

查询时根据需求传入查询条件和分页信息。
SpringBoot中基于JPA的多条件动态查询封装
到此就可以了。只需要在查询类中属性上方添加对应的查询注解,传入接口就好了。不用再写好长好长的代码,减少冗余。是不是happy多了。

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

相关标签: SpringDataJPA