From 1307c564b2114f97d999afa68a496dd60a5feaf5 Mon Sep 17 00:00:00 2001 From: AlexBob Date: Sat, 14 Sep 2024 16:30:13 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(query-utils):=20=E5=BC=95?= =?UTF-8?q?=E5=85=A5=20QueryCondition=20=E8=AE=B0=E5=BD=95=E7=B1=BB?= =?UTF-8?q?=E5=9E=8B=E5=B9=B6=E4=BC=98=E5=8C=96=E5=8A=A8=E6=80=81=20SQL=20?= =?UTF-8?q?=E6=9E=84=E5=BB=BA=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加 `QueryCondition` 记录类型以清晰表达查询条件。 - 在 `QueryJsonHelper` 中增强错误处理与 JSON 查询路径构建。 - 优化 `QueryHelper` 中的条件生成逻辑,支持特殊关键字处理与参数化构建。 - 调整 `LoggerRequest` 的 `buildQueryFragment` 方法以适配新逻辑。 --- .../commons/utils/query/QueryCondition.java | 23 +++ .../boot/commons/utils/query/QueryHelper.java | 141 ++++++++++-------- .../commons/utils/query/QueryJsonHelper.java | 87 +++++++---- .../boot/relational/logger/LoggerRequest.java | 12 +- 4 files changed, 159 insertions(+), 104 deletions(-) create mode 100644 boot/platform/src/main/java/com/plate/boot/commons/utils/query/QueryCondition.java diff --git a/boot/platform/src/main/java/com/plate/boot/commons/utils/query/QueryCondition.java b/boot/platform/src/main/java/com/plate/boot/commons/utils/query/QueryCondition.java new file mode 100644 index 00000000..e58c972b --- /dev/null +++ b/boot/platform/src/main/java/com/plate/boot/commons/utils/query/QueryCondition.java @@ -0,0 +1,23 @@ +package com.plate.boot.commons.utils.query; + +import java.util.Map; + +/** + * Represents a single condition for a database query, encapsulating the operation, + * the corresponding SQL fragment, and the parameters required for the query. + *

+ * This record is primarily used within SQL construction logic to dynamically + * build WHERE clauses based on provided criteria, supporting various comparison + * and set-based operations through its components. + * + * @param operation A Map.Entry consisting of a keyword indicating the type of operation (e.g., equality, range) + * and a placeholder or specific SQL syntax related to the operation. + * @param sql The SQL fragment representing the condition without actual values, + * with placeholders for parameters. + * @param params A map mapping parameter placeholders used in the SQL fragment to their intended values. + */ +public record QueryCondition( + String sql, + Map params, + Map.Entry operation) { +} \ No newline at end of file diff --git a/boot/platform/src/main/java/com/plate/boot/commons/utils/query/QueryHelper.java b/boot/platform/src/main/java/com/plate/boot/commons/utils/query/QueryHelper.java index 84763735..301e5e25 100644 --- a/boot/platform/src/main/java/com/plate/boot/commons/utils/query/QueryHelper.java +++ b/boot/platform/src/main/java/com/plate/boot/commons/utils/query/QueryHelper.java @@ -2,7 +2,6 @@ import com.google.common.base.CaseFormat; import com.google.common.collect.Maps; -import com.google.common.collect.Sets; import com.plate.boot.commons.utils.BeanUtils; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; @@ -20,7 +19,8 @@ */ public final class QueryHelper { - public static final Set SKIP_CRITERIA_KEYS = Set.of("extend", "createdTime", "updatedTime"); + public static final Set SKIP_CRITERIA_KEYS = Set.of("extend", + "createdTime", "updatedTime", "securityCode", "query"); /** * Applies pagination to a SQL query string based on the provided {@link Pageable} object. @@ -78,20 +78,20 @@ public static String applySort(Sort sort, String prefix) { } /** - * Constructs a ParamSql instance representing a part of an SQL WHERE clause - * and its corresponding parameters extracted from a given Java object, while - * providing options to skip certain keys and apply a prefix to the keys. - * The method first converts the object into a map and processes a special - * "query" key using QueryJson to generate SQL and parameters. It then handles - * a "securityCode" key if present and not skipped. Afterward, it filters out - * keys that should be skipped, including default ones defined in SKIP_CRITERIA_KEYS - * and combines these with additional parameters generated from the remaining object map. + * Constructs a QueryFragment for dynamic SQL WHERE clause generation based on an object's properties. + * Excludes specified keys and allows for an optional prefix to be applied to column names. + * This method also processes a special "query" property within the object, which contains + * a nested map of conditions, and applies additional security-related conditions if present. * - * @param object The source object from which SQL conditions and parameters are derived. - * @param skipKeys A collection of keys to be excluded from processing, can be null. - * @param prefix A prefix to prepend to the keys in the generated SQL, useful for nested queries. - * @return A ParamSql object containing a StringJoiner with concatenated SQL conditions - * (joined by 'and') and a map of parameters for prepared statement binding. + * @param object The source object whose properties will be used to construct the query conditions. + * Properties should map to filter values. A special property "query" can be used + * to pass a nested map of conditions. + * @param skipKeys A collection of strings representing property names to exclude from the query conditions. + * These properties will not be included in the generated WHERE clause. + * @param prefix An optional prefix to prepend to each property name in the SQL query, + * useful for specifying table aliases or namespaces. + * @return A QueryFragment containing the concatenated SQL WHERE conditions and a map of parameters. + * The SQL conditions are joined by 'and', and the parameters map binds placeholders to actual values. */ @SuppressWarnings("unchecked") public static QueryFragment query(Object object, Collection skipKeys, String prefix) { @@ -102,35 +102,41 @@ public static QueryFragment query(Object object, Collection skipKeys, St if (ObjectUtils.isEmpty(objectMap)) { return QueryFragment.of(whereSql, bindParams); } - QueryFragment jsonQueryFragment = QueryJsonHelper.queryJson((Map) objectMap.get("query"), prefix); - whereSql.merge(jsonQueryFragment.sql()); - bindParams.putAll(jsonQueryFragment.params()); - String securityCodeKey = "securityCode"; - if (!skipKeys.contains(securityCodeKey) && !ObjectUtils.isEmpty(objectMap.get(securityCodeKey))) { - String key = "tenant_code"; - if (StringUtils.hasLength(prefix)) { - key = prefix + "." + key; - } - whereSql.add(key + " like :securityCode"); - bindParams.put(securityCodeKey, objectMap.get(securityCodeKey)); - } - Set removeKeys = new HashSet<>(SKIP_CRITERIA_KEYS); - removeKeys.add("query"); - removeKeys.add(securityCodeKey); - if (!ObjectUtils.isEmpty(skipKeys)) { - removeKeys.addAll(skipKeys); + if (objectMap.containsKey("query")) { + var jsonMap = (Map) objectMap.get("query"); + QueryFragment jsonQueryFragment = QueryJsonHelper.queryJson(jsonMap, prefix); + whereSql.merge(jsonQueryFragment.sql()); + bindParams.putAll(jsonQueryFragment.params()); } - objectMap = Maps.filterKeys(objectMap, key -> !removeKeys.contains(key)); - QueryFragment entityQueryFragment = query(objectMap, prefix); + String securityCodeKey = "securityCode"; + if (!skipKeys.contains(securityCodeKey) && objectMap.containsKey(securityCodeKey)) { + var condition = securityCondition(objectMap.get(securityCodeKey), prefix); + whereSql.add(condition.sql()); + bindParams.putAll(condition.params()); + } + + objectMap = Maps.filterKeys(objectMap, key -> !SKIP_CRITERIA_KEYS.contains(key) && !skipKeys.contains(key)); + if (!ObjectUtils.isEmpty(objectMap)) { + QueryFragment entityQueryFragment = query(objectMap, prefix); + whereSql.merge(entityQueryFragment.sql()); + bindParams.putAll(entityQueryFragment.params()); + } - whereSql.merge(entityQueryFragment.sql()); - bindParams.putAll(entityQueryFragment.params()); return QueryFragment.of(whereSql, bindParams); } + private static QueryCondition securityCondition(Object value, String prefix) { + String key = "tenant_code"; + if (StringUtils.hasLength(prefix)) { + key = prefix + "." + key; + } + return new QueryCondition(key + " like :securityCode", + Map.of("securityCode", value), null); + } + /** * Constructs a ParamSql instance for dynamic SQL WHERE clause generation * based on a provided map of column-value pairs. Supports optional prefixing @@ -151,25 +157,42 @@ public static QueryFragment query(Object object, Collection skipKeys, St public static QueryFragment query(Map objectMap, String prefix) { StringJoiner whereSql = new StringJoiner(" and "); for (Map.Entry entry : objectMap.entrySet()) { - String column = CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, entry.getKey()); - if (StringUtils.hasLength(prefix)) { - column = prefix + "." + column; - } - - Object value = entry.getValue(); - String paramName = ":" + entry.getKey(); - - if (value instanceof String) { - whereSql.add(column + " like " + paramName); - } else if (value instanceof Collection) { - whereSql.add(column + " in (" + paramName + ")"); - } else { - whereSql.add(column + " = " + paramName); - } + QueryCondition condition = buildCondition(entry, prefix); + whereSql.add(condition.sql()); } return QueryFragment.of(whereSql, objectMap); } + /** + * Constructs a QueryCondition based on a map entry consisting of a column name and its value. + * The method dynamically determines the SQL condition (LIKE, IN, or EQ) based on the value's type, + * applies an optional prefix to the column name, and prepares the condition for use in a query. + * + * @param entry A map entry where the key is the column name in camelCase format + * and the value is the filter criterion which can be a String, Collection, or other type. + * @param prefix An optional prefix to prepend to the column name, useful for specifying table aliases or namespaces. + * @return A QueryCondition object containing: + * - The operation keyword and its associated SQL syntax as a Map.Entry. + * - The SQL fragment representing the condition with placeholders for parameters. + * - A map of parameters mapping placeholders to the actual filter values. + */ + public static QueryCondition buildCondition(Map.Entry entry, String prefix) { + String sql = CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, entry.getKey()); + if (StringUtils.hasLength(prefix)) { + sql = prefix + "." + sql; + } + Object value = entry.getValue(); + String paramName = ":" + entry.getKey(); + if (value instanceof String) { + sql = sql + " like " + paramName; + } else if (value instanceof Collection) { + sql = sql + " in (" + paramName + ")"; + } else { + sql = sql + " = " + paramName; + } + return new QueryCondition(sql, Map.of(paramName, value), null); + } + /** * Constructs a Criteria instance by converting the provided object into a map, * excluding specified keys, and then further processing this map to create @@ -177,20 +200,14 @@ public static QueryFragment query(Map objectMap, String prefix) * SKIP_CRITERIA_KEYS set as well as any additional keys specified in the * skipKes collection from the object map before constructing the Criteria. * - * @param object The Java object to convert into Criteria. Its properties will form the basis of the Criteria. - * @param skipKes A collection of strings representing keys to exclude from the object during conversion. - * These are in addition to the default skipped keys predefined in SKIP_CRITERIA_KEYS. + * @param object The Java object to convert into Criteria. Its properties will form the basis of the Criteria. + * @param skipKeys A collection of strings representing keys to exclude from the object during conversion. + * These are in addition to the default skipped keys predefined in SKIP_CRITERIA_KEYS. * @return A Criteria instance representing the processed object, excluding the specified keys. */ - public static Criteria criteria(Object object, Collection skipKes) { + public static Criteria criteria(Object object, Collection skipKeys) { Map objectMap = BeanUtils.beanToMap(object, true); - if (!ObjectUtils.isEmpty(objectMap)) { - Set mergeSet = Sets.newHashSet(SKIP_CRITERIA_KEYS); - if (!ObjectUtils.isEmpty(skipKes)) { - mergeSet.addAll(skipKes); - } - mergeSet.forEach(objectMap::remove); - } + objectMap = Maps.filterKeys(objectMap, key -> !SKIP_CRITERIA_KEYS.contains(key) && skipKeys.contains(key)); return criteria(objectMap); } diff --git a/boot/platform/src/main/java/com/plate/boot/commons/utils/query/QueryJsonHelper.java b/boot/platform/src/main/java/com/plate/boot/commons/utils/query/QueryJsonHelper.java index 65e720c7..41027cb9 100644 --- a/boot/platform/src/main/java/com/plate/boot/commons/utils/query/QueryJsonHelper.java +++ b/boot/platform/src/main/java/com/plate/boot/commons/utils/query/QueryJsonHelper.java @@ -2,6 +2,7 @@ import com.google.common.base.CaseFormat; import com.google.common.collect.Maps; +import com.plate.boot.commons.exception.RestServerException; import org.springframework.data.domain.Sort; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -93,7 +94,7 @@ private static Sort.Order convertSortOrderToCamelCase(Sort.Order order) { String sortedProperty = CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, keys[0]); int lastIndex = keys.length - 1; if (lastIndex > 0) { - sortedProperty = sortedProperty + appendJsonPathKeys(Arrays.copyOfRange(keys, 1, lastIndex)) + sortedProperty = sortedProperty + buildJsonQueryPath(Arrays.copyOfRange(keys, 1, lastIndex)) .append("->>'").append(keys[lastIndex]).append("'"); } return Sort.Order.by(sortedProperty).with(order.getDirection()); @@ -119,9 +120,9 @@ public static QueryFragment queryJson(Map params, String prefix) return QueryFragment.of(whereSql, bindParams); } for (Map.Entry entry : params.entrySet()) { - QueryCondition exps = prepareQueryPathAndParameters(entry, prefix); - whereSql.add(exps.sql); - bindParams.putAll(exps.params); + QueryCondition exps = buildJsonCondition(entry, prefix); + whereSql.add(exps.sql()); + bindParams.putAll(exps.params()); } return QueryFragment.of(whereSql, bindParams); } @@ -137,55 +138,83 @@ public static QueryFragment queryJson(Map params, String prefix) * - Value: A list of strings representing the parameter names to bind values to in the SQL prepared statement. * @throws IllegalArgumentException If the keys array is null or empty. */ - private static QueryCondition prepareQueryPathAndParameters(Map.Entry entry, String prefix) { + private static QueryCondition buildJsonCondition(Map.Entry entry, String prefix) { String[] keys = StringUtils.delimitedListToStringArray(entry.getKey(), "."); + if (keys.length < 2) { + throw RestServerException.withMsg("Json query column path [query[" + entry.getKey() + "]] error", + List.of("Json path example: extend.username", + "Request query params:", + "query[extend.usernameLike]=aa", + "query[extend.age]=23", + "query[extend.nameIn]=aa,bb,cc", + "query[extend.codeEq]=123456" + )); + } // 处理第一个键 - String column = CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, keys[0]); + String sql = CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, keys[0]); if (StringUtils.hasLength(prefix)) { - column = prefix + "." + keys[0]; + sql = prefix + "." + keys[0]; } // 构建 JSON 路径 - StringBuilder jsonPath = new StringBuilder("(" + column); + StringBuilder jsonPath = new StringBuilder("(" + sql); + int lastIndex = keys.length - 1; // 处理中间键 if (lastIndex > 1) { String[] joinKeys = Arrays.copyOfRange(keys, 1, lastIndex); - jsonPath.append(appendJsonPathKeys(joinKeys)); + jsonPath.append(buildJsonQueryPath(joinKeys)); } //处理最后键 - return processLastKeyExpr(keys, entry.getValue(), jsonPath); + QueryCondition lastCondition = buildLastCondition(keys, entry.getValue()); + sql = jsonPath.append(lastCondition.sql()).toString(); + return new QueryCondition(sql, lastCondition.params(), lastCondition.operation()); } - private static QueryCondition processLastKeyExpr(String[] keys, Object value, StringBuilder jsonPath) { + /** + * Constructs the final part of a query condition based on the provided keys and a value. + * This method handles special keywords for operations such as 'Between', 'NotBetween', 'In', and 'NotIn', + * formatting the SQL condition accordingly and preparing the necessary bind parameters. + * + * @param keys An array of strings forming the path to the JSON attribute. The last key may contain + * a keyword indicating a special operation. + * @param value The value or values to be used in the query condition. For 'In' and 'NotIn' + * operations, this should be a comma-separated string or collection of values. + * @return A {@link QueryCondition} object containing: + * - The SQL fragment representing the condition with placeholders for parameters. + * - A map of parameter names to their bound values. + * - The operation details including the keyword and its SQL representation. + */ + private static QueryCondition buildLastCondition(String[] keys, Object value) { + StringBuilder conditionSql = new StringBuilder("->>'"); + String paramName = StringUtils.arrayToDelimitedString(keys, "_"); String lastKey = keys[keys.length - 1]; + Map.Entry exps = retrieveKeywordMapping(lastKey); if (exps == null) { - jsonPath.append("->>'").append(lastKey).append("' = :").append(paramName); - return new QueryCondition(SQL_OPERATION_MAPPING.entrySet().iterator().next(), - jsonPath.append(")").toString(), Map.of(paramName, value)); + conditionSql.append(lastKey).append("' = :").append(paramName); + return new QueryCondition(conditionSql.append(")").toString(), Map.of(paramName, value), + SQL_OPERATION_MAPPING.entrySet().iterator().next()); } + String key = lastKey.substring(0, lastKey.length() - exps.getKey().length()); - jsonPath.append("->>'").append(key).append("' "); + conditionSql.append(key).append("' "); if ("Between".equals(exps.getKey()) || "NotBetween".equals(exps.getKey())) { String startKey = paramName + "_start"; String endKey = paramName + "_end"; - jsonPath.append(exps.getValue()).append(" :").append(startKey).append(" and :").append(endKey); + conditionSql.append(exps.getValue()).append(" :").append(startKey).append(" and :").append(endKey); var values = StringUtils.commaDelimitedListToStringArray(String.valueOf(value)); - return new QueryCondition(exps, jsonPath.append(")").toString(), - Map.of(startKey, values[0], endKey, values[1])); + return new QueryCondition(conditionSql.append(")").toString(), + Map.of(startKey, values[0], endKey, values[1]), exps); } else if ("NotIn".equals(exps.getKey()) || "In".equals(exps.getKey())) { - jsonPath.append(exps.getValue()).append(" (:").append(paramName).append(")"); + conditionSql.append(exps.getValue()).append(" (:").append(paramName).append(")"); var values = StringUtils.commaDelimitedListToSet(String.valueOf(value)); - return new QueryCondition(exps, jsonPath.append(")").toString(), - Map.of(paramName, values)); + return new QueryCondition(conditionSql.append(")").toString(), Map.of(paramName, values), exps); } else { - jsonPath.append(exps.getValue()).append(" :").append(paramName); - return new QueryCondition(exps, jsonPath.append(")").toString(), - Map.of(paramName, value)); + conditionSql.append(exps.getValue()).append(" :").append(paramName); + return new QueryCondition(conditionSql.append(")").toString(), Map.of(paramName, value), exps); } - } /** @@ -195,7 +224,7 @@ private static QueryCondition processLastKeyExpr(String[] keys, Object value, St * @param joinKeys An array of strings representing intermediate keys in a JSON path. * @return StringBuilder containing the concatenated intermediate keys formatted for a JSON path expression. */ - private static StringBuilder appendJsonPathKeys(String[] joinKeys) { + private static StringBuilder buildJsonQueryPath(String[] joinKeys) { StringBuilder jsonPath = new StringBuilder(); for (String path : joinKeys) { jsonPath.append("->'").append(path).append("'"); @@ -221,9 +250,5 @@ private static Map.Entry retrieveKeywordMapping(String inputStr) }).orElse(null); } - private record QueryCondition( - Map.Entry operation, - String sql, - Map params) { - } + } \ No newline at end of file diff --git a/boot/platform/src/main/java/com/plate/boot/relational/logger/LoggerRequest.java b/boot/platform/src/main/java/com/plate/boot/relational/logger/LoggerRequest.java index 606ad696..b144dbf4 100644 --- a/boot/platform/src/main/java/com/plate/boot/relational/logger/LoggerRequest.java +++ b/boot/platform/src/main/java/com/plate/boot/relational/logger/LoggerRequest.java @@ -7,12 +7,9 @@ import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; -import org.springframework.util.ObjectUtils; -import java.util.Arrays; import java.util.List; import java.util.Map; -import java.util.StringJoiner; /** * Represents a logging request that extends the basic {@link Logger} functionality, @@ -59,13 +56,6 @@ public Logger toLogger() { } public QueryFragment buildQueryFragment() { - QueryFragment fragment = QueryHelper.query(this, List.of("operator"), null); - StringJoiner criteria = fragment.sql(); - Map params = fragment.params(); - if (!ObjectUtils.isEmpty(this.getOperator())) { - criteria.add("operator in (:operator)"); - params.put("operator", Arrays.asList("a", "b")); - } - return QueryFragment.of(criteria, params); + return QueryHelper.query(this, List.of(), null); } } \ No newline at end of file