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

Posted c.

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了可配置化的表达式解析以及构造JSON查询数据库实体数据的设计和实现相关的知识,希望对你有一定的参考价值。

文章目录

可配置化的表达式解析以及构造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手撸一个ORM使用说明

多态模型可绑定表达式树解析器

使用Spring Boot JPA Specification实现使用JSON数据来查询实体数据

[Go] 轻量服务器框架全局配置的实现以及解析json

ESP32-IDF开发实例-JSON数据构造与解析

Json解析两种方法以及U3d配置安卓环境