一个很神奇的【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应该使用什么样的条件进行查询,有大于小于这些范围查询,有前模糊,后模糊,模糊查询,有区间等(可继续扩展)。

一个很神奇的【JPAUtil.class】_第1张图片

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

一个很神奇的【JPAUtil.class】_第2张图片

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 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 criteriaClass = criteria.getClass();
        Field[] fields = criteriaClass.getDeclaredFields();
        List 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 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  Specification defaultSpec(Class clazz) {
        return (root, query, criteriaBuilder) -> criteriaBuilder.conjunction();
    }

    /**
     * 返回BaseCriteria对应拥有JPASpecification注解的非空字段的 fieldName:QueryCriteriaDTO 的键值对集合
     *
     * @param criteria 查询条件
     * @return 键值对集合
     * @throws IllegalAccessException 非法访问异常(此处为获取)
     */
    public static HashMap opDTOFromCriteria(BaseCriteria criteria) throws IllegalAccessException {
        HashMap result = new HashMap<>(16);
        Class 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> opMap, QueryCriteriaDTO dto, BaseCriteria.OPEnum op) {
        List list;
        if (opMap.containsKey(op)) {
            list = opMap.get(op);
        } else {
            list = new ArrayList<>();
        }
        list.add(dto);
        opMap.put(op, list);
    }

    /**
     * ①通过JPASpecificationFromCriteria()方法获取非空字段的->dbo的fieldName:JPASpecification注解的键值对集合
* ② * * @param criteria 查询条件 * @param criteriaClass BaseCriteria对应的子类的class * @return 键值对集合 * @throws IllegalAccessException 非法访问异常(此处为获取) */ public static HashMap> fieldsFromCriteria(BaseCriteria criteria, Class criteriaClass) throws IllegalAccessException { // 操作集合 HashMap> opMap = new HashMap<>(8); // HashMap 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 Specification specFromCriteria(Class clazz, BaseCriteria criteria, Class criteriaClass) throws IllegalAccessException { HashMap> opMap = fieldsFromCriteria(criteria, criteriaClass); return (Root root, CriteriaQuery query, CriteriaBuilder criteriaBuilder) -> { // List 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 Predicate buildPredicate(HashMap> opMap, BaseCriteria.OPEnum op, Root root, CriteriaQuery query, CriteriaBuilder criteriaBuilder) { List dtoList = opMap.get(op); List 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 example = Example.of(new Ttt().setTtt(criteria.getTtt()), exampleMatcher);
        List 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)

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

你可能感兴趣的:(SpringBoot,JPA,java,jpa)