可配置化的表达式解析以及构造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 AND ( 2 OR 3 ) )
,然后我们通过空格把我们的表达式split分解成数组[(, 1,AND, (, 2, OR, 3,) ,)]
-
第二步我们会用到栈来解析表达式,我们会把上述的数组一个个入栈,当遇到右括号
)
的时候就开始把栈内的数据弹出,直到遇到左括号(
后停止, 取出来的这部分的数据(2 OR 3)
其实就是一个组合的表达式,我们就需要这个组合的表达式构造成一个新的ConditionDTO
,然后再重新放入栈中,然后继续把数组剩余的数据入栈…
下面我们通过图解的方式来看看:
我们先把[(, 1,AND, (, 2, OR, 3,) ,)]
的数据入栈
当我们遇到了右括号)
这个时候就开始弹出栈顶元素,直到遇到左括号(
。于是就取出来了(2 OR 3)
, 这个时候就会把(2 OR 3)
组合成一个新的conditionDTO对象,也就是把表达式2
和表达式3
用OR
来组合,然后变成一个新的表达式,于是变成这样一个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 AND 1)
-
表达式中的序号要跟表格中的序号全等,也就是一样的序号,不存在哪边的多出来其他的序号
-
当遇到右括号的时候,需要弹出栈中的内容,当弹到栈内元素为空的时候,说明缺少左括号
-
当弹出栈内元素到最后,栈内元素的个数大于1,说明最后剩余的除了最后的
ConditionDTO
,还有剩余的左括号,所以这个时候说明缺少了右括号 -
当出现
(1 OR 2 AND 3)
这种括号包裹的表达式内同时存在AND
和OR
的操作符,则校验不通过,因为在数据库中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使用说明