一个很神奇的【JPAUtil.class】

背景描述:

这个是前几个月突发奇想去构建的一个关于JPA查询的工具类,具体包含了几个注解,一个Base查询类,和一个工具类。

真正使用的时候,需要将base类进行继承并扩展,那么接下来上代码。


第一部分,三个注解:

第一个注解@JPASort,该注解用于标识字段,被标识的字段必须填充filedName属性来确定排序使用的字段,且该字段需要是boolean类型。(后面会有示例)

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface JPASort {
    String filedName();
}

第二个注解@JPASpecification,该注解用于标识字段,被标识的字段必须填充filedName属性。用于指明是区间查询、单体查询、条件查询等

例:通过接口入参tttOP(OPEnum在后面的Base类中)来标识ttt应该使用什么样的条件进行查询,有大于小于这些范围查询,有前模糊,后模糊,模糊查询,有区间等(可继续扩展)。

 例:创建时间的区间查询如下↓↓↓ 

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface JPASpecification {
    String filedName();
    /**
     * 是否仅支持equal条件,默认不使用,如果此属性为true,则不考虑其他匹配条件
     */
    boolean onlyEq() default false;
    /**
     * filedName 对应的 QueryCriteria
     */
    String queryCriteria() default "";
    /**
     * 是否是区间的开头
     */
    boolean startOfSection() default false;
    /**
     * 是否是区间的尾部
     */
    boolean endOfSection() default false;
}

第三个注解,@JPAExampleMatcher,该注解用于标识字段,被标识的字段必须填充filedName属性。使用Example查询,在后面有示例。

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 灵活匹配只支持String类型,其他类型只支持精准匹配,所以只有String类型需要使用该注解
 */
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface JPAExampleMatcher {
    String filedName();
    boolean eq() default false;
    boolean fuzzyQuery() default false;
    boolean startsWith() default false;
    boolean endsWith() default false;
    boolean ignoreFiled() default false;
}

第二部分,base类:

该类内部只有一个OPEnum枚举,不包含其他内容。

import java.util.HashMap;
import java.util.Map;

/**
 * 继承该类,指明子类是一个查询条件
 */
public class BaseCriteria {

    public enum OPEnum {
        EQ(1, "等于"),
        NEQ(2,"不等于"),
        GT(3, "大于"),
        GT_OR_EQ(4,"大于等于"),
        LT(5,"小于"),
        LT_OR_EQ(6,"小于等于"),
        START(7,"模糊查询-以...开始"),
        END(8,"模糊查询-以...结束"),
        CONTAINS(9,"模糊查询-包含"),
        BETWEEN(10, "区间"),
        ;

        private final int code;
        private final String description;

        OPEnum(int code, String description) {
            this.code = code;
            this.description = description;
        }

        public String description() {
            return this.description;
        }

        public int getCode() {
            return code;
        }

        public static String getMsgByCodeInt(int codeInt) {
            for (OPEnum e : OPEnum.values()) {
                if (e.getCode() == codeInt) {
                    return e.description;
                }
            }
            throw new IllegalArgumentException("未定义的code码:" + codeInt);
        }

        private static final Map<Integer, OPEnum> OP_ENUM_MAP = new HashMap<>();

        static {
            for (OPEnum a : OPEnum.values()) {
                OP_ENUM_MAP.put(a.getCode(), a);
            }
        }

        /**
         * 通过code获取OPEnum
         *
         * @param code code
         * @return OPEnum
         */
        public static OPEnum getEnumByCode(Integer code) {
            return OP_ENUM_MAP.get(code);
        }

    }

}

第三部分,util:

import cn.hutool.core.util.ArrayUtil;
import JPAExampleMatcher;
import JPASort;
import JPASpecification;
import BaseCriteria;
import lombok.Data;
import lombok.experimental.Accessors;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.ExampleMatcher;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.util.ReflectionUtils;

import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Objects;

@Slf4j
public class JPAUtils {

    /**
     * 通过BaseCriteria中的排序条件组装「排序对象」,如果没有排序条件,则返回无排序设置的Sort
     *
     * @param criteria 查询条件
     * @return 排序对象
     * @throws IllegalAccessException 非法访问异常(此处为获取)
     */
    public static Sort sortFromCriteria(BaseCriteria criteria) throws IllegalAccessException {
        if (criteria == null) {
            return Sort.unsorted();
        }
        Class<? extends BaseCriteria> criteriaClass = criteria.getClass();
        Field[] fields = criteriaClass.getDeclaredFields();
        List<Sort.Order> orderList = new ArrayList<>();
        for (Field field : fields) {
            ReflectionUtils.makeAccessible(field);
            // 对标注了JPASort注解的字段且字段值不为null的,进行Order拼装
            if (field.isAnnotationPresent(JPASort.class) && Objects.nonNull(field.get(criteria))) {
                JPASort jpaSort = field.getAnnotation(JPASort.class);
                boolean desc = (Boolean) field.get(criteria);
                Sort.Order order = desc ? Sort.Order.desc(jpaSort.filedName()) : Sort.Order.asc(jpaSort.filedName());
                orderList.add(order);
            }
        }
        if (orderList.size() > 0) {
            return Sort.by(orderList);
        }
        return Sort.unsorted();
    }


    /**
     * 通过BaseCriteria中example部分的字段标注的注解 @JPAExampleMatcher 组装ExampleMatcher
     * 注意:灵活匹配只针对于String类型,其余类型只能精确匹配,所以完全动态的查询ExampleMatcher并不支持
     *
     * @param criteria 查询条件
     * @return ExampleMatcher
     */
    public static ExampleMatcher exampleMatcherFromCriteria(BaseCriteria criteria) {
        ExampleMatcher matcher = ExampleMatcher.matching().withIgnoreCase();
        if (criteria == null) {
            return matcher;
        }
        Class<? extends BaseCriteria> criteriaClass = criteria.getClass();
        Field[] fields = criteriaClass.getDeclaredFields();
        for (Field field : fields) {
            // 对标注了JPAExampleMatcher注解的字段,进行matcher拼装
            if (field.isAnnotationPresent(JPAExampleMatcher.class)) {
                JPAExampleMatcher jpaExampleMatcher = field.getAnnotation(JPAExampleMatcher.class);
                // 根据标注的不同查询条件,进行匹配规则
                if (jpaExampleMatcher.fuzzyQuery()) {
                    matcher = matcher.withMatcher(jpaExampleMatcher.filedName(), ExampleMatcher.GenericPropertyMatcher::contains); // 模糊匹配
                }
                if (jpaExampleMatcher.startsWith()) {
                    matcher = matcher.withMatcher(jpaExampleMatcher.filedName(), ExampleMatcher.GenericPropertyMatcher::startsWith); // 开头匹配
                }
                if (jpaExampleMatcher.endsWith()) {
                    matcher = matcher.withMatcher(jpaExampleMatcher.filedName(), ExampleMatcher.GenericPropertyMatcher::endsWith); // 末尾匹配
                }
                if (jpaExampleMatcher.eq()) {
                    matcher = matcher.withMatcher(jpaExampleMatcher.filedName(), ExampleMatcher.GenericPropertyMatcher::exact); // 精确匹配
                }
                if (jpaExampleMatcher.ignoreFiled()) {
                    matcher = matcher.withIgnorePaths(jpaExampleMatcher.filedName()); // 忽略哪些字段不作为匹配规则
                }
            }
        }
        return matcher;
    }

    /**
     * 返回最基本的Specification
     */
    public static <T> Specification<T> defaultSpec(Class<T> clazz) {
        return (root, query, criteriaBuilder) -> criteriaBuilder.conjunction();
    }

    /**
     * 返回BaseCriteria对应拥有JPASpecification注解的非空字段的 fieldName:QueryCriteriaDTO 的键值对集合
     *
     * @param criteria 查询条件
     * @return 键值对集合
     * @throws IllegalAccessException 非法访问异常(此处为获取)
     */
    public static HashMap<String, QueryCriteriaDTO> opDTOFromCriteria(BaseCriteria criteria) throws IllegalAccessException {
        HashMap<String, QueryCriteriaDTO> result = new HashMap<>(16);
        Class<? extends BaseCriteria> criteriaClass = criteria.getClass();
        Field[] fields = criteriaClass.getDeclaredFields();
        for (Field field : fields) {
            ReflectionUtils.makeAccessible(field);
            if (field.isAnnotationPresent(JPASpecification.class) && Objects.nonNull(field.get(criteria))) {
                JPASpecification jpaSpecification = field.getAnnotation(JPASpecification.class);
                Object value = field.get(criteria);
                QueryCriteriaDTO dto;
                // 如果是区间
                if (jpaSpecification.startOfSection()) {
                    dto = result.containsKey(jpaSpecification.filedName()) ? result.get(jpaSpecification.filedName()) : new QueryCriteriaDTO()
                            .setSingle(false)
                            .setFieldName(jpaSpecification.filedName())
                            .setStartValue(value.toString())
                            .setStartJpaSpecification(jpaSpecification);
                } else if (jpaSpecification.endOfSection()) {
                    dto = result.containsKey(jpaSpecification.filedName()) ? result.get(jpaSpecification.filedName()) : new QueryCriteriaDTO()
                            .setSingle(false)
                            .setFieldName(jpaSpecification.filedName())
                            .setEndValue(value.toString())
                            .setEndJpaSpecification(jpaSpecification);
                } else {
                    // 如果是单体
                    dto = new QueryCriteriaDTO()
                            .setSingle(true)
                            .setSingleJpaSpecification(jpaSpecification)
                            .setFieldName(jpaSpecification.filedName())
                            .setFieldValue(value.toString());
                }
                // 放入
                result.put(jpaSpecification.filedName(), dto);
            }
        }
        return result;
    }

    public static void putFieldToOpMap(HashMap<BaseCriteria.OPEnum, List<QueryCriteriaDTO>> opMap, QueryCriteriaDTO dto, BaseCriteria.OPEnum op) {
        List<QueryCriteriaDTO> list;
        if (opMap.containsKey(op)) {
            list = opMap.get(op);
        } else {
            list = new ArrayList<>();
        }
        list.add(dto);
        opMap.put(op, list);
    }

    /**
     * ①通过JPASpecificationFromCriteria()方法获取非空字段的->dbo的fieldName:JPASpecification注解的键值对集合<br/>
     * ②
     *
     * @param criteria      查询条件
     * @param criteriaClass BaseCriteria对应的子类的class
     * @return 键值对集合
     * @throws IllegalAccessException 非法访问异常(此处为获取)
     */
    public static <K> HashMap<BaseCriteria.OPEnum, List<QueryCriteriaDTO>> fieldsFromCriteria(BaseCriteria criteria, Class<K> criteriaClass) throws IllegalAccessException {
        // 操作集合
        HashMap<BaseCriteria.OPEnum, List<QueryCriteriaDTO>> opMap = new HashMap<>(8);
        //
        HashMap<String, QueryCriteriaDTO> fieldJPASpecMap = opDTOFromCriteria(criteria);
        for (String fieldName : fieldJPASpecMap.keySet()) {
            QueryCriteriaDTO queryCriteriaDTO = fieldJPASpecMap.get(fieldName);
            // 单体
            if (queryCriteriaDTO.getSingle()) {
                // 只能是EQ的,放入EQ操作中后,开始下一个字段
                if (queryCriteriaDTO.getSingleJpaSpecification().onlyEq()) {
                    putFieldToOpMap(opMap, queryCriteriaDTO, BaseCriteria.OPEnum.EQ);
                    continue;
                }
                // 其他操作:
                // 在请求DTO中该字段对应的查询条件字段名
                String queryCriteria = queryCriteriaDTO.getSingleJpaSpecification().queryCriteria();
                // 根据此字段名获得字段的值
                Field field = ReflectionUtils.findField(criteriaClass, queryCriteria);
                assert field != null;
                ReflectionUtils.makeAccessible(field);
                Object op = ReflectionUtils.getField(field, criteria);
                if (op instanceof BaseCriteria.OPEnum) {
                    putFieldToOpMap(opMap, queryCriteriaDTO, (BaseCriteria.OPEnum) op);
                }
            } else {
                // 区间BETWEEN操作
                if (Objects.nonNull(queryCriteriaDTO.getStartValue()) && Objects.nonNull(queryCriteriaDTO.getEndValue())) {
                    putFieldToOpMap(opMap, queryCriteriaDTO, BaseCriteria.OPEnum.BETWEEN);
                }
                // 其他非单体操作(如果有,在此处进行添加 ↓↓↓)

            }
        }
        return opMap;
    }


    public static <T, K> Specification<T> specFromCriteria(Class<T> clazz, BaseCriteria criteria, Class<K> criteriaClass) throws IllegalAccessException {
        HashMap<BaseCriteria.OPEnum, List<QueryCriteriaDTO>> opMap = fieldsFromCriteria(criteria, criteriaClass);
        return (Root<T> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder) -> {
            //
            List<Predicate> predicateList = new ArrayList<>();
            // 根据opMap组装Predicate然后返回
            for (BaseCriteria.OPEnum op : opMap.keySet()) {
                // 每一种操作组装成一个 Predicate
                predicateList.add(buildPredicate(opMap, op, root, query, criteriaBuilder));
            }
            if (predicateList.size() > 0) {
                return criteriaBuilder.and(ArrayUtil.toArray(predicateList, Predicate.class));
            }
            return criteriaBuilder.conjunction();
        };
    }

    private static <T> Predicate buildPredicate(HashMap<BaseCriteria.OPEnum, List<QueryCriteriaDTO>> opMap, BaseCriteria.OPEnum op, Root<T> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder) {
        List<QueryCriteriaDTO> dtoList = opMap.get(op);
        List<Predicate> predicateList = new ArrayList<>();
        for (QueryCriteriaDTO criteriaDTO : dtoList) {
            Predicate predicate;
            switch (op) {
                case EQ:
                    predicateList.add(criteriaBuilder.equal(root.get(criteriaDTO.getFieldName()), criteriaDTO.getFieldValue()));
                    break;
                case NEQ:
                    predicateList.add(criteriaBuilder.notEqual(root.get(criteriaDTO.getFieldName()), criteriaDTO.getFieldValue()));
                    break;
                case GT:
                    predicateList.add(criteriaBuilder.greaterThan(root.get(criteriaDTO.getFieldName()), criteriaDTO.getFieldValue()));
                    break;
                case GT_OR_EQ:
                    predicateList.add(criteriaBuilder.greaterThanOrEqualTo(root.get(criteriaDTO.getFieldName()), criteriaDTO.getFieldValue()));
                    break;
                case LT:
                    predicateList.add(criteriaBuilder.lessThan(root.get(criteriaDTO.getFieldName()), criteriaDTO.getFieldValue()));
                    break;
                case LT_OR_EQ:
                    predicateList.add(criteriaBuilder.lessThanOrEqualTo(root.get(criteriaDTO.getFieldName()), criteriaDTO.getFieldValue()));
                    break;
                case START:
                    predicateList.add(criteriaBuilder.like(root.get(criteriaDTO.getFieldName()), criteriaDTO.getFieldValue() + "%"));
                    break;
                case END:
                    predicateList.add(criteriaBuilder.like(root.get(criteriaDTO.getFieldName()), "%" + criteriaDTO.getFieldValue()));
                    break;
                case CONTAINS:
                    predicateList.add(criteriaBuilder.like(root.get(criteriaDTO.getFieldName()), "%" + criteriaDTO.getFieldValue() + "%"));
                    break;
                case BETWEEN:
                    predicateList.add(criteriaBuilder.between(root.get(criteriaDTO.getFieldName()), criteriaDTO.getStartValue(), criteriaDTO.getEndValue()));
                    break;
                default:
                    log.warn("暂不支持的操作类型" + op.description());
                    break;
            }
        }
        return criteriaBuilder.and(ArrayUtil.toArray(predicateList, Predicate.class));
    }

    @Data
    @Accessors(chain = true)
    public static class QueryCriteriaDTO {
        private String fieldName;
        // 单体查询条件使用fieldValue
        private String fieldValue;
        // 区别是单体还是区间:true->单体;false->区间
        private Boolean single;
        private JPASpecification singleJpaSpecification;
        // 区间查询条件使用startValue和endValue
        private String startValue;
        private String endValue;
        private JPASpecification startJpaSpecification;
        private JPASpecification endJpaSpecification;
    }


}

第四部分,使用:

首先我们需要有一个继承了base类的查询类,其中Example部分对应dbo中的字段:

@Data
@EqualsAndHashCode(callSuper = true)
public class CriteriaTest extends BaseCriteria {

    /**
     * Sort部分:用于order by指定字段,此处可以任意保留0-n个,一般建议0-2个排序条件即可
     * (该部分内容非必须,但是一旦指定,默认true是desc,false是asc)
     */
    @JPASort(filedName = "createTime")
    private Boolean createTimeDesc;
    @JPASort(filedName = "updateTime")
    private Boolean updateTimeDesc;
    @JPASort(filedName = "status")
    private Boolean statusDesc;

    /**
     * Example部分:通过属性值组装example进行查询
     * 通过 @JPAExampleMatcher 标注进行组装匹配规则(灵活匹配只支持字符串类型,其余类型只支持精准匹配)
     * 下面的id表示必须精准匹配,ttt和descript可以是灵活匹配,根据是否输入该查询条件进行匹配
     */
    @JPAExampleMatcher(filedName = "id", eq = true)
    private String id;
    @JPAExampleMatcher(filedName = "ttt", fuzzyQuery = true)
    private String ttt;
    private Integer status;
    private Date createTime;
    private Date updateTime;
    @JPAExampleMatcher(filedName = "descript", fuzzyQuery = true)
    private String descript;
}

然后是我们的test:

    @Test
    @Rollback
    void exampleTest() {
        CriteriaTest criteria = new CriteriaTest();
        criteria.setTtt("1");
        ExampleMatcher exampleMatcher = JPAUtils.exampleMatcherFromCriteria(criteria);
        Example<Ttt> example = Example.of(new Ttt().setTtt(criteria.getTtt()), exampleMatcher);
        List<Ttt> list = tttRepo.findAll(example);
        list.forEach(System.out::println);
    }

因为数据库里面只有一条数据,最后test输出的结果【toString()】是:

Ttt(ttt=ff4b6ca5cb8246218cdccab89f02d24c, status=1, createTime=2022-11-11 14:16:51.895, updateTime=2022-11-11 14:16:51.895, descript=init)

更多查询相关的使用,如分页查询、动态查询可以自己摸索哦~

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值