first commit
This commit is contained in:
@@ -0,0 +1,328 @@
|
||||
package com.metalloop.common.utils;
|
||||
|
||||
import cn.hutool.core.text.CharSequenceUtil;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.core.conditions.interfaces.Compare;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.metalloop.common.enums.Direction;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.beans.BeanWrapperImpl;
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
import java.beans.PropertyDescriptor;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* MyBatis-Plus QueryWrapper 条件构造器,从查询对象中构建查询条件
|
||||
* 条件构造遵循以下约定:
|
||||
* <p>
|
||||
* 1.解析规则
|
||||
* <p>
|
||||
* 1.1.查询对象字段值非空(不是null和空字符串)才会加入查询条件
|
||||
* <p>
|
||||
* 1.2.查询对象字段对应的数据库字段,通过对应注解 @TableField(value = "client_id") 的value提取
|
||||
* <p>
|
||||
* 1.3.查询对象字段值类型是 Direction,用order by片段,如 Direction detectionTimeOrder = Direction.DESC 会转换为 order by detection_time desc
|
||||
* <p>
|
||||
* 1.4.查询对象字段值类型是 {@link Collection},用in片段,如果以not开头或结尾,使用not in片段如 List<> notSiteCodes 会转换为 site_code not in (?,?,?),否则使用in片段如 List<> siteCodes 会转换为 site_code in (?,?,?)。另请注意:如果集合不为 null 但元素数量为零,不是不加入查询条件,而是加入一个不可能的值,如 in () 会报错,所以会加入 in ('__UNLIKELY_VALUE_300552__'),这样可以避免报错。同时保持字面的含义,即选择了 0 个值,查询结果也为 0 个
|
||||
* <p>
|
||||
* 1.5.查询对象字段名以start|lower|min|from开头或结尾,表示范围的下限,用>=片段,如 detectionTimeStart 会转换为 detection_time >= ?
|
||||
* <p>
|
||||
* 1.6.查询对象字段名以end|upper|max|to开头或结尾,表示范围的上限,用<=片段,如 detectionTimeEnd 会转换为 detection_time <= ?
|
||||
* <p>
|
||||
* 1.7.查询对象字段名以startEc|lowerEc|minEc|fromEc开头或结尾,Ec约定为‘exclusive’的缩写,表示范围的下限,但是不包含当前值,用>片段,如 detectionTimeStartEc 会转换为 detection_time > ?
|
||||
* <p>
|
||||
* 1.8.查询对象字段名以endEc|upperEc|maxEc|toEc开头或结尾,Ec约定为‘exclusive’的缩写,表示范围的上限,但是不包含当前值,用<片段,如 detectionTimeEndEc 会转换为 detection_time < ?
|
||||
* <p>
|
||||
* 1.9.prefix|suffix开头或结尾,表示前缀或后缀查询,用like片段,如 vehPlatePrefix 会转换为 veh_plate like ?% 或 vehPlateSuffix 会转换为 veh_plate like %?
|
||||
* <p>
|
||||
* 1.10.查询对象字段名以notLike|notContains|notMatches|notMatch开头或结尾,表示否定模糊查询,用not like片段,如 vehPlateNotLike 会转换为 veh_plate not like %?%
|
||||
* <p>
|
||||
* 1.11.查询对象字段名以like|contains|matches|match开头或结尾,表示模糊查询,用like片段,如 vehPlateLike 会转换为 veh_plate like %?%
|
||||
* <p>
|
||||
* 1.12.查询对象字段名以fulltext开头或结尾,表示全文检索查询,用MATCH() AGAINST()片段,如 vehPlateFullText 会转换为 MATCH(veh_plate) AGAINST(?)
|
||||
* <p>
|
||||
* 1.13.查询对象字段名以not开头或结尾,表示否定查询,用not片段,如 vehPlateNot 会转换为 veh_plate != ?
|
||||
* <p>
|
||||
* 1.14.其他情况表示精确查询,用=片段,如busiNum 会转换为busi_num = ?
|
||||
* <p>
|
||||
* 2.OR嵌套查询列表
|
||||
* <p>
|
||||
* 需要构建OR嵌套查询列表,解析规则同上,解析结果为嵌套查询,最终将转换为 and (site_code in (?,?,?) or detection_time >= ? or ...)
|
||||
*
|
||||
* @author zhaowenhao
|
||||
* @since 2022-12-27
|
||||
*/
|
||||
@SuppressWarnings({"AlibabaMethodTooLong", "DuplicatedCode"})
|
||||
public class QueryWrapperBuilder {
|
||||
|
||||
/**
|
||||
* Utility classes should not have a public or default constructor.
|
||||
*/
|
||||
private QueryWrapperBuilder() {
|
||||
throw new IllegalStateException("Utility class");
|
||||
}
|
||||
|
||||
/**
|
||||
* 不可能的值,用于处理集合为空的情况
|
||||
*/
|
||||
public static final String UNLIKELY_VALUE = "__UNLIKELY_VALUE_300552__";
|
||||
|
||||
/**
|
||||
* 范围开始(不包含当前值)正则表达式
|
||||
*/
|
||||
public static final Pattern EXCLUSIVE_RANGE_START_PATTERN = Pattern.compile("(.+(start|lower|min|from)ec)|((start|lower|min|from)ec.+)", Pattern.CASE_INSENSITIVE);
|
||||
|
||||
/**
|
||||
* 范围结束(不包含当前值)正则表达式
|
||||
*/
|
||||
public static final Pattern EXCLUSIVE_RANGE_END_PATTERN = Pattern.compile("(.+(end|upper|max|to)ec)|((end|upper|max|to)ec.+)", Pattern.CASE_INSENSITIVE);
|
||||
|
||||
/**
|
||||
* 范围开始正则表达式
|
||||
*/
|
||||
public static final Pattern RANGE_START_PATTERN = Pattern.compile("(.+(start|lower|min|from))|((start|lower|min|from).+)", Pattern.CASE_INSENSITIVE);
|
||||
|
||||
/**
|
||||
* 范围结束正则表达式
|
||||
*/
|
||||
public static final Pattern RANGE_END_PATTERN = Pattern.compile("(.+(end|upper|max|to))|((end|upper|max|to).+)", Pattern.CASE_INSENSITIVE);
|
||||
|
||||
/**
|
||||
* 前缀查询
|
||||
*/
|
||||
public static final Pattern PREFIX_PATTERN = Pattern.compile("(.+(prefix))|((prefix).+)", Pattern.CASE_INSENSITIVE);
|
||||
|
||||
/**
|
||||
* 后缀查询
|
||||
*/
|
||||
public static final Pattern SUFFIX_PATTERN = Pattern.compile("(.+(suffix))|((suffix).+)", Pattern.CASE_INSENSITIVE);
|
||||
|
||||
/**
|
||||
* 模糊查询
|
||||
*/
|
||||
public static final Pattern LIKE_PATTERN = Pattern.compile("(.+(like|contains|matches|match))|((like|contains|matches|match).+)", Pattern.CASE_INSENSITIVE);
|
||||
|
||||
/**
|
||||
* 否定模糊查询
|
||||
*/
|
||||
public static final Pattern NOT_LIKE_PATTERN = Pattern.compile("(.+(not)(like|contains|matches|match))|((not)(like|contains|matches|match).+)", Pattern.CASE_INSENSITIVE);
|
||||
|
||||
/**
|
||||
* 全文检索查询
|
||||
*/
|
||||
public static final Pattern FULL_TEXT_SEARCH_PATTERN = Pattern.compile("(.+(fulltext))|((fulltext).+)", Pattern.CASE_INSENSITIVE);
|
||||
|
||||
/**
|
||||
* 不等于查询
|
||||
*/
|
||||
public static final Pattern NOT_PATTERN = Pattern.compile("(.+(not))|((not).+)", Pattern.CASE_INSENSITIVE);
|
||||
|
||||
/**
|
||||
* 特定JSON值查询
|
||||
*/
|
||||
public static final Pattern JSON_CONTAINS_PATTERN = Pattern.compile("(.+(jsonContains))|((jsonContains).+)", Pattern.CASE_INSENSITIVE);
|
||||
|
||||
@FunctionalInterface
|
||||
public interface QueryStrategy {
|
||||
default void apply(QueryWrapper<?> queryWrapper, String column, Object value, boolean isOr) {
|
||||
if (isOr) {
|
||||
queryWrapper.or();
|
||||
}
|
||||
apply(queryWrapper, column, value);
|
||||
}
|
||||
|
||||
void apply(QueryWrapper<?> wrapper, String column, Object value);
|
||||
}
|
||||
|
||||
// region 封装正则表达式Map和策略Map
|
||||
private static final Map<String, QueryStrategy> STRATEGY_MAP = new HashMap<>();
|
||||
private static final Map<String, Pattern> PATTERN_MAP = new HashMap<>();
|
||||
|
||||
public static final String RANGE_START_PATTERN_KEY = "RANGE_START_PATTERN";
|
||||
|
||||
public static final String RANGE_END_PATTERN_KEY = "RANGE_END_PATTERN";
|
||||
|
||||
public static final String EXCLUSIVE_RANGE_START_PATTERN_KEY = "EXCLUSIVE_RANGE_START_PATTERN";
|
||||
|
||||
public static final String EXCLUSIVE_RANGE_END_PATTERN_KEY = "EXCLUSIVE_RANGE_END_PATTERN";
|
||||
|
||||
public static final String PREFIX_PATTERN_KEY = "PREFIX_PATTERN";
|
||||
|
||||
public static final String SUFFIX_PATTERN_KEY = "SUFFIX_PATTERN";
|
||||
|
||||
public static final String NOT_LIKE_PATTERN_KEY = "NOT_LIKE_PATTERN";
|
||||
|
||||
public static final String LIKE_PATTERN_KEY = "LIKE_PATTERN";
|
||||
|
||||
public static final String FULL_TEXT_SEARCH_PATTERN_KEY = "FULL_TEXT_SEARCH_PATTERN";
|
||||
|
||||
public static final String NOT_PATTERN_KEY = "NOT_PATTERN";
|
||||
|
||||
public static final String JSON_CONTAINS_PATTERN_KEY = "JSON_CONTAINS_PATTERN";
|
||||
|
||||
static {
|
||||
PATTERN_MAP.put(RANGE_START_PATTERN_KEY, RANGE_START_PATTERN);
|
||||
PATTERN_MAP.put(RANGE_END_PATTERN_KEY, RANGE_END_PATTERN);
|
||||
PATTERN_MAP.put(EXCLUSIVE_RANGE_START_PATTERN_KEY, EXCLUSIVE_RANGE_START_PATTERN);
|
||||
PATTERN_MAP.put(EXCLUSIVE_RANGE_END_PATTERN_KEY, EXCLUSIVE_RANGE_END_PATTERN);
|
||||
PATTERN_MAP.put(PREFIX_PATTERN_KEY, PREFIX_PATTERN);
|
||||
PATTERN_MAP.put(SUFFIX_PATTERN_KEY, SUFFIX_PATTERN);
|
||||
PATTERN_MAP.put(NOT_LIKE_PATTERN_KEY, NOT_LIKE_PATTERN);
|
||||
PATTERN_MAP.put(LIKE_PATTERN_KEY, LIKE_PATTERN);
|
||||
PATTERN_MAP.put(FULL_TEXT_SEARCH_PATTERN_KEY, FULL_TEXT_SEARCH_PATTERN);
|
||||
PATTERN_MAP.put(NOT_PATTERN_KEY, NOT_PATTERN);
|
||||
|
||||
|
||||
STRATEGY_MAP.put(RANGE_START_PATTERN_KEY, Compare::ge);
|
||||
STRATEGY_MAP.put(RANGE_END_PATTERN_KEY, Compare::le);
|
||||
STRATEGY_MAP.put(EXCLUSIVE_RANGE_START_PATTERN_KEY, Compare::gt);
|
||||
STRATEGY_MAP.put(EXCLUSIVE_RANGE_END_PATTERN_KEY, Compare::lt);
|
||||
STRATEGY_MAP.put(PREFIX_PATTERN_KEY, Compare::likeRight);
|
||||
STRATEGY_MAP.put(SUFFIX_PATTERN_KEY, Compare::likeLeft);
|
||||
STRATEGY_MAP.put(NOT_LIKE_PATTERN_KEY, Compare::notLike);
|
||||
STRATEGY_MAP.put(LIKE_PATTERN_KEY, Compare::like);
|
||||
STRATEGY_MAP.put(FULL_TEXT_SEARCH_PATTERN_KEY, (queryWrapper, column, value) -> queryWrapper.apply("MATCH(" + column + ") AGAINST({0})", value));
|
||||
STRATEGY_MAP.put(NOT_PATTERN_KEY, Compare::ne);
|
||||
STRATEGY_MAP.put(JSON_CONTAINS_PATTERN_KEY, (queryWrapper, column, value) ->
|
||||
queryWrapper.apply("JSON_CONTAINS(" + column + ", CAST({0} AS JSON))", value)
|
||||
);
|
||||
}
|
||||
// endregion
|
||||
|
||||
@SafeVarargs
|
||||
public static <Q, T> QueryWrapper<T> build(Q andQuery, Q... orQueryArray) {
|
||||
if (andQuery == null) return null;
|
||||
|
||||
QueryWrapper<T> queryWrapper = new QueryWrapper<>();
|
||||
applyQuery(queryWrapper, andQuery, false);
|
||||
|
||||
Optional.ofNullable(orQueryArray).ifPresent(array -> Arrays.stream(array)
|
||||
.filter(Objects::nonNull)
|
||||
.forEach(orQuery -> queryWrapper.and(qw -> applyQuery(qw, orQuery, true))));
|
||||
|
||||
return queryWrapper;
|
||||
}
|
||||
|
||||
private static <Q, T> void applyQuery(QueryWrapper<T> queryWrapper, Q query, boolean isOr) {
|
||||
Class<?> aClass = query.getClass();
|
||||
PropertyDescriptor[] propertyDescriptors = getPropertyDescriptors(aClass);
|
||||
final BeanWrapperImpl wrapper = new BeanWrapperImpl(query);
|
||||
|
||||
for (PropertyDescriptor propertyDescriptor : propertyDescriptors) {
|
||||
String column = getColumn(aClass, propertyDescriptor);
|
||||
String propertyName = propertyDescriptor.getName();
|
||||
Object propertyValue = wrapper.getPropertyValue(propertyName);
|
||||
// 先处理特殊值:排序标志和集合
|
||||
if (propertyValue instanceof Direction) {
|
||||
applyOrder(queryWrapper, column, propertyValue);
|
||||
} else if (propertyValue instanceof Collection<?>) {
|
||||
// 如果集合为空,需要特殊处理为一个无意义的值,否则会导致 in () 报错
|
||||
if (((Collection<?>) propertyValue).isEmpty()) {
|
||||
propertyValue = Collections.singletonList(UNLIKELY_VALUE);
|
||||
}
|
||||
applyCollection(queryWrapper, column, propertyName, (Collection<?>) propertyValue);
|
||||
} else if (propertyValue != null && CharSequenceUtil.isNotBlank(propertyValue.toString())) {
|
||||
applyStrategy(queryWrapper, column, propertyName, propertyValue, isOr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static <T> void applyOrder(QueryWrapper<T> queryWrapper, String column, Object value) {
|
||||
if (Direction.ASC.equals(value)) {
|
||||
queryWrapper.orderByAsc(column);
|
||||
} else {
|
||||
queryWrapper.orderByDesc(column);
|
||||
}
|
||||
}
|
||||
|
||||
private static <T> void applyCollection(QueryWrapper<T> queryWrapper, String column, String propertyName, Collection<?> values) {
|
||||
if (PATTERN_MAP.get(NOT_PATTERN_KEY).matcher(propertyName).matches()) {
|
||||
queryWrapper.notIn(column, values);
|
||||
} else {
|
||||
queryWrapper.in(column, values);
|
||||
}
|
||||
}
|
||||
|
||||
private static <T> void applyStrategy(QueryWrapper<T> queryWrapper, String column, String propertyName, Object propertyValue, boolean isOr) {
|
||||
Optional<QueryStrategy> matchedStrategy = findMatchingStrategy(propertyName);
|
||||
if (matchedStrategy.isPresent() && !propertyName.equalsIgnoreCase(CharSequenceUtil.toCamelCase(column))) {
|
||||
matchedStrategy.get().apply(queryWrapper, column, propertyValue, isOr);
|
||||
} else {
|
||||
applyDefaultStrategy(queryWrapper, column, propertyValue, isOr);
|
||||
}
|
||||
}
|
||||
|
||||
private static Optional<QueryStrategy> findMatchingStrategy(String propertyName) {
|
||||
return STRATEGY_MAP.entrySet().stream()
|
||||
.filter(entry -> {
|
||||
Pattern pattern = PATTERN_MAP.get(entry.getKey());
|
||||
return pattern != null && pattern.matcher(propertyName).matches();
|
||||
})
|
||||
.findFirst()
|
||||
.map(Map.Entry::getValue);
|
||||
}
|
||||
|
||||
private static <T> void applyDefaultStrategy(QueryWrapper<T> queryWrapper, String column, Object value, boolean isOr) {
|
||||
if (isOr) {
|
||||
queryWrapper.or().eq(column, value);
|
||||
} else {
|
||||
queryWrapper.eq(column, value);
|
||||
}
|
||||
}
|
||||
|
||||
// region 处理缓存的Class的PropertyDescriptor和column的映射关系
|
||||
|
||||
static final ConcurrentMap<Class<?>, CachedColumnResults> CLASS_CACHED_COLUMN_RESULTS_MAP =
|
||||
new ConcurrentHashMap<>(128);
|
||||
private static final PropertyDescriptor[] EMPTY_PROPERTY_DESCRIPTOR_ARRAY = {};
|
||||
|
||||
public static PropertyDescriptor[] getPropertyDescriptors(Class<?> clazz) throws BeansException {
|
||||
return forClass(clazz).getPropertyDescriptors();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static String getColumn(Class<?> clazz, PropertyDescriptor pd) throws BeansException {
|
||||
return forClass(clazz).getColumn(pd);
|
||||
}
|
||||
|
||||
static CachedColumnResults forClass(Class<?> beanClass) throws BeansException {
|
||||
CachedColumnResults results = CLASS_CACHED_COLUMN_RESULTS_MAP.get(beanClass);
|
||||
if (results != null) {
|
||||
return results;
|
||||
}
|
||||
|
||||
results = new CachedColumnResults(beanClass);
|
||||
|
||||
CachedColumnResults existing = CLASS_CACHED_COLUMN_RESULTS_MAP.putIfAbsent(beanClass, results);
|
||||
return (existing != null ? existing : results);
|
||||
}
|
||||
|
||||
public static final class CachedColumnResults {
|
||||
|
||||
private final Map<PropertyDescriptor, String> pdToColumnMap;
|
||||
|
||||
private CachedColumnResults(Class<?> beanClass) throws BeansException {
|
||||
pdToColumnMap = Arrays.stream(beanClass.getDeclaredFields())
|
||||
.filter(field -> field.isAnnotationPresent(TableField.class))
|
||||
.collect(Collectors.toMap(field -> BeanUtils.getPropertyDescriptor(beanClass, field.getName()),
|
||||
field -> field.getAnnotation(TableField.class).value()));
|
||||
}
|
||||
|
||||
@Nullable
|
||||
String getColumn(PropertyDescriptor pd) {
|
||||
return pdToColumnMap.get(pd);
|
||||
}
|
||||
|
||||
PropertyDescriptor[] getPropertyDescriptors() {
|
||||
return this.pdToColumnMap.keySet().toArray(EMPTY_PROPERTY_DESCRIPTOR_ARRAY);
|
||||
}
|
||||
|
||||
}
|
||||
// endregion
|
||||
}
|
||||
Reference in New Issue
Block a user