可配置化的表达式解析以及构造JSON查询数据库实体数据的设计和实现

本文档详细介绍了如何实现可配置的表达式解析,通过前端配置的表达式和数据,构造JSON查询数据库实体数据。利用栈数据结构解析如1AND(2OR3)的表达式,确保表达式的正确性和完整性,并通过校验避免冲突。最后,通过后端将解析后的JSON用于查询数据库,实现类似SQL的动态查询功能。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

可配置化的表达式解析以及构造JSON查询数据库实体数据的设计和实现

之前的博文《使用Spring Boot JPA Specification实现使用JSON数据来查询实体数据》《使用Druid SQL Parser解析SQL》中都讲到了目前业务上的需求就是以前老系统是通过配置SQL去抽取一些业务数据的,但现在新系统想通过页面的一些配置化实现跟配置SQL一样去抽取数据。

之前的后端实现逻辑已经在之前《使用Spring Boot JPA Specification实现使用JSON数据来查询实体数据》的博文中讲解了,本篇博文主要是结合前端去实现。

最终前端的可配置效果如下:

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

就是前端可以选择具体的哪些字段进行过滤,然后过滤的操作符是什么,过滤的值是什么,然后是否对当前的操作取反,比如当前的操作符如果是=,取反设置为Yes,这表示!=。然后在最下方的组合方面,可以对上面表格中的数据进行and或者or的组合,或者使用自定义的组合方式。

前端是用React写的,具体页面怎么实现这里不是重点,我们重点在于前端数据与后端交互,以及后端如何根据前端给出来的数据,构造出我们想要的json结构,然后通过之前后端实现的功能,用这个json去查询数据库实体数据。

前端传入到后端的数据其实包含两部分,一部分就是expression表达式,也就是上面的(1 AND 2) OR 3,上面的1,2,3表示的是表格中的序号,然后第二部分就是表格中的数据会以一个list数组的方式给后端。然后后端会根据表达式和整个list数组构造出来json

可配置化的表达式解析

首先在我们之前的博文说过,conditionDTO对象的json结构是如下的:

{
    "conditions": [{
        "conditions": [{
            "conditions": [],
            "operation": null,
            "conditionExpression": {
                "type": "STRING",
                "column": "name",
                "operateExpression": "=",
                "not": false,
                "operateValue": ["test"]

            }
    }, {
            "conditions": [],
            "operation": null,
            "conditionExpression": {
                "type": "NUMBER",
                "column": "age",
                "operateExpression": "=",
                "not": false,
                "operateValue": ["14"],
                "dateformat": null,
                "dateFormatFunction": null
            }
    }],
        "operation": "OR"
    }, {
        "conditions": [],
        "operation": null,
        "conditionExpression": {
            "type": "NUMBER",
            "column": "id",
            "operateExpression": "=",
            "not": false,
            "operateValue": ["1"]
        }
    }],
    "operation": "AND",
    "conditionExpression": null
}

假设我们现在有一个表达式1 AND (2 OR 3) , 其中1,2,3 是也是表达式,比如表达式1可能就是上面的json条件的name='test'

所以表达式1是一个ConditionDTO对象, 表达式(2 OR 3)也是一个ConditionDTO,只不过是组合起来的ConditionDTO对昂, 表达式1 AND (2 OR 3)也一样是一个ConditionDTO对象, 这不过它是一个更加复杂的组合。

生成出来的ConditionDTO会类似于:

{
    "conditions": [{
            "conditions": [],
            "operation": null,
            "conditionExpression": {
                "type": "STRING",
                "column": "1",
                "operateExpression": "=",
                "not": false,
                "operateValue": ["1"],
                "dateformat": null,
                "dateFormatFunction": null
            }
    },
        {
            "conditions": [{
                "conditions": [],
                "operation": null,
                "conditionExpression": {
                    "type": "STRING",
                    "column": "2",
                    "operateExpression": "=",
                    "not": false,
                    "operateValue": ["2"],
                    "dateformat": null,
                    "dateFormatFunction": null
                }
    }, {
                "conditions": [],
                "operation": null,
                "conditionExpression": {
                    "type": "STRING",
                    "column": "3",
                    "operateExpression": "=",
                    "not": false,
                    "operateValue": ["3"],
                    "dateformat": null,
                    "dateFormatFunction": null
                }
    }],
            "operation": "OR",
            "conditionExpression": null

    }],
    "operation": "AND",
    "conditionExpression": null
}

在这里插入图片描述

所以我们现在要做的就是如何去解析这样的一个表达式 1 AND (2 OR 3)

我们这里会用到这种数据结构来解析表达式,栈内的每一个数据,我们都把他看成一个表达式,也就是一个ConditionDTO对象

  1. 第一步我们先会预处理表达式,去掉表达式多余的空格,给表达式两边加入左右括号,通过空格隔断我们的表达式,上面的表达式在预处理之后会最终会变成( 1 AND ( 2 OR 3 ) ),然后我们通过空格把我们的表达式split分解成数组[(, 1,AND, (, 2, OR, 3,) ,)]

  2. 第二步我们会用到栈来解析表达式,我们会把上述的数组一个个入栈,当遇到右括号)的时候就开始把栈内的数据弹出,直到遇到左括号(后停止, 取出来的这部分的数据(2 OR 3)其实就是一个组合的表达式,我们就需要这个组合的表达式构造成一个新的ConditionDTO,然后再重新放入栈中,然后继续把数组剩余的数据入栈…

下面我们通过图解的方式来看看:

我们先把[(, 1,AND, (, 2, OR, 3,) ,)]的数据入栈

在这里插入图片描述

当我们遇到了右括号)这个时候就开始弹出栈顶元素,直到遇到左括号(。于是就取出来了(2 OR 3), 这个时候就会把(2 OR 3)组合成一个新的conditionDTO对象,也就是把表达式2表达式3OR来组合,然后变成一个新的表达式,于是变成这样一个json

{
    "conditions": [{
        "conditions": [],
        "operation": null,
        "conditionExpression": {
            "type": "STRING",
            "column": "2",
            "operateExpression": "=",
            "not": false,
            "operateValue": ["2"],
            "dateformat": null,
            "dateFormatFunction": null
        }
    }, {
        "conditions": [],
        "operation": null,
        "conditionExpression": {
            "type": "STRING",
            "column": "3",
            "operateExpression": "=",
            "not": false,
            "operateValue": ["3"],
            "dateformat": null,
            "dateFormatFunction": null
        }
    }],
    "operation": "OR",
    "conditionExpression": null

}

然后在把这个构造出来的新对象放回到栈中。

在这里插入图片描述

然后我们继续[(, 1,AND, (, 2, OR, 3,) ,)]中剩余的数据入栈

在这里插入图片描述

这个时候又遇到了),于是就会弹出来(1 AND '(2 OR 3)'). 这个时候就把1 和之前(2 OR 3)构造出来的新对象用AND组合,形成一个新的对象,也就是变成上面最终的json

{
    "conditions": [{
            "conditions": [],
            "operation": null,
            "conditionExpression": {
                "type": "STRING",
                "column": "1",
                "operateExpression": "=",
                "not": false,
                "operateValue": ["1"],
                "dateformat": null,
                "dateFormatFunction": null
            }
    },
        {
            "conditions": [{
                "conditions": [],
                "operation": null,
                "conditionExpression": {
                    "type": "STRING",
                    "column": "2",
                    "operateExpression": "=",
                    "not": false,
                    "operateValue": ["2"],
                    "dateformat": null,
                    "dateFormatFunction": null
                }
    }, {
                "conditions": [],
                "operation": null,
                "conditionExpression": {
                    "type": "STRING",
                    "column": "3",
                    "operateExpression": "=",
                    "not": false,
                    "operateValue": ["3"],
                    "dateformat": null,
                    "dateFormatFunction": null
                }
    }],
            "operation": "OR",
            "conditionExpression": null

    }],
    "operation": "AND",
    "conditionExpression": null
}

然后继续放回去栈顶,那此刻栈顶唯一的那个元素就是我们最后构造出来的conditionDTO对象了。

在这里插入图片描述

然后我们就可以通过这个conditionDTO对象去查询我们的数据库实体数据了。

那么我们在这个过程中会有一些校验,如下:

  1. 表达式中的序号不能重复,比如不能出现(1 AND 1)

  2. 表达式中的序号要跟表格中的序号全等,也就是一样的序号,不存在哪边的多出来其他的序号

  3. 当遇到右括号的时候,需要弹出栈中的内容,当弹到栈内元素为空的时候,说明缺少左括号

  4. 当弹出栈内元素到最后,栈内元素的个数大于1,说明最后剩余的除了最后的ConditionDTO,还有剩余的左括号,所以这个时候说明缺少了右括号

  5. 当出现 (1 OR 2 AND 3) 这种括号包裹的表达式内同时存在 ANDOR 的操作符,则校验不通过,因为在数据库中AND的优先级会更高,所以在逻辑处理和语义上可能存在冲突,所以建议写成(1 OR (2 AND 3)) 或者(1 OR 2) AND 3)

构造JSON查询数据库实体数据

最后这里就是代码的实现了,至于最后怎么通过JSON数据去查询数据库实体数据可以参考之前的博文《使用Spring Boot JPA Specification实现使用JSON数据来查询实体数据》

@Builder
@Data
@AllArgsConstructor
@NoArgsConstructor
public class RuleDetailDTO {

  private String id;

  @NotNull
  private ColumnType type;

  @NotBlank
  private String columnName;

  @NotNull
  private OperateExpressionEnum operateExpression;

  private boolean not;

  private String operateValue;

  private String dateformat;

  private String dbDateFormat;

  @NotNull
  private Long sequence;
}

@UtilityClass
public class ExpressionParser {


  public static ConditionDTO parseExpression(String expression, List<RuleDetailDTO> ruleDetails) throws ConditionValidationException {
    if (CollectionUtils.isEmpty(ruleDetails)) {
      return null;
    }
    List<ConditionNodeDTO> expressions = preprocessExpression(expression);
    validateRuleSequence(expressions, ruleDetails);
    Deque<ConditionNodeDTO> deque = new LinkedList<>();
    for (ConditionNodeDTO exp : expressions) {
      deque.push(exp);
      if (StringUtils.equalsIgnoreCase(RIGHT_PARENTHESIS, Objects.requireNonNull(deque.peek()).getValue())) {
        deque.push(handleSubExpression(ruleDetails, deque));
      }
    }
    ConditionAssertUtils.isTrue(deque.size() == 1, INCORRECT_EXPRESSION_NOT_CLOSE_LEFT_PARENTHESES);
    return deque.pop().getCondition();
  }

  private static List<ConditionNodeDTO> preprocessExpression(String expression) {
    if (StringUtils.isEmpty(expression)) {
      return Collections.emptyList();
    }
    return Arrays.stream(getExpression(String.format("(%s)", expression)).toUpperCase(Locale.ROOT).split(StringUtils.SPACE))
        .filter(StringUtils::isNotBlank).map(StringUtils::trim)
        .map(item -> ConditionNodeDTO.builder().value(item).build()).collect(Collectors.toList());
  }


  private String getExpression(String expression) {
    return expression.replace(LEFT_PARENTHESIS, SPACES + LEFT_PARENTHESIS + SPACES)
        .replace(RIGHT_PARENTHESIS, SPACES + RIGHT_PARENTHESIS + SPACES)
        .replace(AND, SPACES + AND + SPACES)
        .replace(OR, SPACES + OR + SPACES);
  }


  private void validateRuleSequence(List<ConditionNodeDTO> expressions, List<RuleDetailDTO> ruleDetails) throws ConditionValidationException {
    List<String> sequenceList = getSequenceList(expressions).stream().map(ConditionNodeDTO::getValue).collect(Collectors.toList());
    ConditionAssertUtils.notEmpty(sequenceList, INCORRECT_EXPRESSION_NOT_INCLUDE_SEQUENCE);
    boolean hasDuplicateSequence = sequenceList.stream().distinct().count() < sequenceList.size();
    boolean isCongruent = new HashSet<>(ruleDetails.stream().map(item -> String.valueOf(item.getSequence()))
        .collect(Collectors.toList())).containsAll(sequenceList)
        && new HashSet<>(sequenceList).containsAll(ruleDetails.stream().map(item -> String.valueOf(item.getSequence()))
        .collect(Collectors.toList()));
    ConditionAssertUtils.isFalse(hasDuplicateSequence, INCORRECT_EXPRESSION_DUPLICATE_SEQUENCE);
    ConditionAssertUtils.isFalse(!isCongruent, INCORRECT_EXPRESSION_INCOMPLETE_SEQUENCE);
  }

  private List<ConditionNodeDTO> getSequenceList(List<ConditionNodeDTO> expressions) {
    return expressions.stream()
        .filter(item -> !StringUtils.equalsIgnoreCase(LEFT_PARENTHESIS, item.getValue())
            && !StringUtils.equalsIgnoreCase(RIGHT_PARENTHESIS, item.getValue())
            && !StringUtils.equalsIgnoreCase(AND, item.getValue())
            && !StringUtils.equalsIgnoreCase(OR, item.getValue())).collect(Collectors.toList());
  }


  private static ConditionNodeDTO handleSubExpression(List<RuleDetailDTO> ruleDetails, Deque<ConditionNodeDTO> deque) {
    List<ConditionNodeDTO> currentExpressions = new ArrayList<>();
    while (true) {
      ConditionNodeDTO pop = deque.pop();
      currentExpressions.add(pop);
      if (StringUtils.equalsIgnoreCase(LEFT_PARENTHESIS, pop.getValue())) {
        break;
      }
      ConditionAssertUtils.isFalse(deque.isEmpty(), INCORRECT_EXPRESSION_NOT_CLOSE_RIGHT_PARENTHESES);
    }
    return generateConditionNode(currentExpressions, ruleDetails);
  }

  private static ConditionNodeDTO generateConditionNode(List<ConditionNodeDTO> expressions, List<RuleDetailDTO> ruleDetails)
      throws ConditionValidationException {
    List<String> expressionValues = expressions.stream().map(ConditionNodeDTO::getValue).collect(Collectors.toList());
    ConditionAssertUtils.isFalse(expressionValues.contains(AND) && expressionValues.contains(OR),
        INCORRECT_EXPRESSION_NOT_SUPPORT);
    List<ConditionNodeDTO> sequenceConditionNodeListDTO = getSequenceList(expressions);
    ConditionAssertUtils.notEmpty(sequenceConditionNodeListDTO, INCORRECT_EXPRESSION_NOT_INCLUDE_SEQUENCE);
    if (sequenceConditionNodeListDTO.size() == 1 && Objects.nonNull(sequenceConditionNodeListDTO.get(0).getCondition())) {
      return sequenceConditionNodeListDTO.get(0);
    }
    OperationEnum operation = null;
    if (expressionValues.contains(AND)) {
      operation = OperationEnum.AND;
    } else if (expressionValues.contains(OR)) {
      operation = OperationEnum.OR;
    }
    ConditionAssertUtils.isFalse(Objects.isNull(operation) && ruleDetails.size() > 1, INCORRECT_EXPRESSION_MISSING_OPERATE);
    ConditionDTO condition = parseRuleDetailToConditionDTO(ruleDetails, expressionValues, operation);
    List<ConditionDTO> conditionList = expressions.stream().map(ConditionNodeDTO::getCondition).filter(Objects::nonNull)
        .collect(Collectors.toList());
    if (Objects.nonNull(condition)) {
      conditionList.add(condition);
    }
    return ConditionNodeDTO.builder().condition(ConditionDTO.getConditionDTOByOperation(conditionList, operation))
        .value(String.format("(%s)", expressions.stream().map(ConditionNodeDTO::getValue).collect(Collectors.joining(SPACES)))).build();
  }

  private static ConditionDTO parseRuleDetailToConditionDTO(List<RuleDetailDTO> ruleDetails, List<String> expressionValues,
      OperationEnum operation) {
    ConditionDTO condition;
    List<RuleDetailDTO> details = ruleDetails.stream().filter(item -> expressionValues.contains(String.valueOf(item.getSequence())))
        .collect(Collectors.toList());
    if (!CollectionUtils.isEmpty(details) && details.size() == 1) {
      condition = ConditionDTO.fromRules(details);
    } else {
      condition = ConditionDTO.fromRules(details, operation);
    }
    return condition;
  }


  @Data
  @Builder
  @AllArgsConstructor
  @NoArgsConstructor
  private static class ConditionNodeDTO {

    private String value;
    private ConditionDTO condition;
  }
}
public static final String SPACES = " ";
public static final String LEFT_PARENTHESIS = "(";
public static final String RIGHT_PARENTHESIS = ")";
public static final String AND = "AND";
public static final String OR = "OR";
public static final String INCORRECT_EXPRESSION_NOT_INCLUDE_SEQUENCE = "Please include sequence number in the expression";
public static final String INCORRECT_EXPRESSION_DUPLICATE_SEQUENCE = "The expression has duplicate sequence number";
public static final String INCORRECT_EXPRESSION_INCOMPLETE_SEQUENCE = "The sequence number in the expression is incomplete";
public static final String INCORRECT_EXPRESSION_NOT_SUPPORT = "Not support expression like: '1 AND 2 OR 3'; should be like: '(1 AND 2) OR 3' or '1 AND (2 OR 3)'";
public static final String INCORRECT_EXPRESSION_MISSING_OPERATE = "The expression must include AND or OR";
public static final String INCORRECT_EXPRESSION_NOT_CLOSE_RIGHT_PARENTHESES = "The right parentheses in the expression are not closed";
public static final String INCORRECT_EXPRESSION_NOT_CLOSE_LEFT_PARENTHESES = "The left parentheses in the expression are not closed";

最后后端在接收到前端传过来的expression表达式,和一个list的RuleDetailDTO之后就会调用ExpressionParser类的parseExpression方法去构造出最后的ConditionDTO对象。之后就可以利用这个ConditionDTO对象去查询数据了

### MyBatis Plus 查询 JSON 示例教程 在现代应用程序开发中,处理复杂的JSON数据结构成为了一项常见任务。为了满足这一需求,在MyBatis-Plus (MP) 中可以采用特定的方式来进行JSON字段的操作。 对于想要执行基于JSON字段的查询操作而言,定义实体类时指定`typeHandler`属性是非常重要的[^2]。这使得能够通过自定义处理器来解析序列化JSON对象到数据库中的TEXT或其他适合存储JSON字符串的数据类型。例如: ```java @TableName(value = "tb_user", autoResultMap = true) public class User { @TableField(typeHandler = JacksonTypeHandler.class) private List<String> tags; } ``` 当涉及到具体的查询逻辑实现方面,假设有一个名为`tags`的列表作为用户的标签集合,并且希望找到所有包含某个特定标签(比如:"technology")在内的用户记录,则可以在Mapper接口里编写如下方法签名: ```java import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import java.util.List; List<User> selectUsersWithTag(String tag); ``` 接着是在对应的XML文件或者是动态SQL构建器内完成实际的SQL语句构造工作。这里给出一种使用QueryWrapper简化条件拼接的例子: ```java // 构建查询条件 QueryWrapper<User> queryWrapper = new QueryWrapper<>(); queryWrapper.like("tags", "\"" + targetTag + "\""); // 执行查询并获取结果集 List<User> usersWithTargetTag = userMapper.selectList(queryWrapper); ``` 上述代码片段展示了如何利用`like`关键字配合双引号包裹的目标标签值进行模糊匹配查询。需要注意的是这种方法依赖于底层数据库引擎对JSON路径表达式的识别能力以及性能表现;不同版本之间可能存在差异因此建议查阅官方文档了解最新特性支持情况[^1]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值