可配置化的表达式解析以及构造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(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
对象去查询数据了