这个是前几个月突发奇想去构建的一个关于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;
}
该类内部只有一个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);
}
}
}
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 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 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 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> 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)
更多查询相关的使用,如分页查询、动态查询可以自己摸索哦~