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 条件构造器,从查询对象中构建查询条件 * 条件构造遵循以下约定: *

* 1.解析规则 *

* 1.1.查询对象字段值非空(不是null和空字符串)才会加入查询条件 *

* 1.2.查询对象字段对应的数据库字段,通过对应注解 @TableField(value = "client_id") 的value提取 *

* 1.3.查询对象字段值类型是 Direction,用order by片段,如 Direction detectionTimeOrder = Direction.DESC 会转换为 order by detection_time desc *

* 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 个 *

* 1.5.查询对象字段名以start|lower|min|from开头或结尾,表示范围的下限,用>=片段,如 detectionTimeStart 会转换为 detection_time >= ? *

* 1.6.查询对象字段名以end|upper|max|to开头或结尾,表示范围的上限,用<=片段,如 detectionTimeEnd 会转换为 detection_time <= ? *

* 1.7.查询对象字段名以startEc|lowerEc|minEc|fromEc开头或结尾,Ec约定为‘exclusive’的缩写,表示范围的下限,但是不包含当前值,用>片段,如 detectionTimeStartEc 会转换为 detection_time > ? *

* 1.8.查询对象字段名以endEc|upperEc|maxEc|toEc开头或结尾,Ec约定为‘exclusive’的缩写,表示范围的上限,但是不包含当前值,用<片段,如 detectionTimeEndEc 会转换为 detection_time < ? *

* 1.9.prefix|suffix开头或结尾,表示前缀或后缀查询,用like片段,如 vehPlatePrefix 会转换为 veh_plate like ?% 或 vehPlateSuffix 会转换为 veh_plate like %? *

* 1.10.查询对象字段名以notLike|notContains|notMatches|notMatch开头或结尾,表示否定模糊查询,用not like片段,如 vehPlateNotLike 会转换为 veh_plate not like %?% *

* 1.11.查询对象字段名以like|contains|matches|match开头或结尾,表示模糊查询,用like片段,如 vehPlateLike 会转换为 veh_plate like %?% *

* 1.12.查询对象字段名以fulltext开头或结尾,表示全文检索查询,用MATCH() AGAINST()片段,如 vehPlateFullText 会转换为 MATCH(veh_plate) AGAINST(?) *

* 1.13.查询对象字段名以not开头或结尾,表示否定查询,用not片段,如 vehPlateNot 会转换为 veh_plate != ? *

* 1.14.其他情况表示精确查询,用=片段,如busiNum 会转换为busi_num = ? *

* 2.OR嵌套查询列表 *

* 需要构建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 STRATEGY_MAP = new HashMap<>(); private static final Map 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 QueryWrapper build(Q andQuery, Q... orQueryArray) { if (andQuery == null) return null; QueryWrapper 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 void applyQuery(QueryWrapper 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 void applyOrder(QueryWrapper queryWrapper, String column, Object value) { if (Direction.ASC.equals(value)) { queryWrapper.orderByAsc(column); } else { queryWrapper.orderByDesc(column); } } private static void applyCollection(QueryWrapper 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 void applyStrategy(QueryWrapper queryWrapper, String column, String propertyName, Object propertyValue, boolean isOr) { Optional 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 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 void applyDefaultStrategy(QueryWrapper 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, 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 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 }