最近碰到一个动态查询条件的问题,比如,前端界面上可选择输入姓名、性别、年龄、电话等查询条件,后端根据是否输入来动态构建查询sql where条件(即如果未输入则不作为查询条件)。如果这些条件最终是全and或全or起来则相对容易处理,但如果既有and又有or则动态构建就困难了。
这个问题经抽象可表达为动态重写逻辑表达式,即先写出完整的表达式,然后排除无效部分。例如,对于下面的完整逻辑表达式字符串:
(a) or (b) and (c)
- 如果 a 无效则重写为 (b) and (c)
- 如果 b 无效则重写为 (a) or (c)
- 如果 c 无效则重写为 (a) or (b)
- 如果 a 和 b 都无效则重写为 (c)
- ...
- 如果全都无效则返回null
这个问题如何通用地解决呢?初看起来似乎很难,无从下手,但用ANTLR来解决则简单到出乎你的意料。
1、定义逻辑表达式语法
1 # LogicExpr.g4文件 2 3 grammar LogicExpr; 4 5 stat: expr ; 6 7 expr: expr AND expr # and 8 | expr OR expr # or 9 | '(' expr ')' # group 10 | VAR # var 11 ; 12 13 VAR : '(' KEY ')' ; 14 15 AND: 'and' ; 16 OR: 'or' ; 17 KEY: [a-zA-Z0-9_]+ ; 18 WS: [ ]+ -> skip ;
2、用ANTLR工具生成Java代码(略)
3、接着写排除算法
1 package com.mycomp.antlr4.le; 2 3 import java.util.Collection; 4 5 import org.antlr.v4.runtime.ANTLRInputStream; 6 import org.antlr.v4.runtime.CommonTokenStream; 7 import org.antlr.v4.runtime.misc.Interval; 8 9 import com.mycomp.antlr4.le.LogicExprParser.AndContext; // 以下5个import是ANTLR生成的代码 10 import com.mycomp.antlr4.le.LogicExprParser.GroupContext; 11 import com.mycomp.antlr4.le.LogicExprParser.OrContext; 12 import com.mycomp.antlr4.le.LogicExprParser.StatContext; 13 import com.mycomp.antlr4.le.LogicExprParser.VarContext; 14 15 public class LogicExprRewriter extends LogicExprBaseVisitor<String> { 16 17 public static String rewrite(String le, Collection<String> invalidKeys) { 18 ANTLRInputStream input = new ANTLRInputStream(le); 19 20 LogicExprLexer lexer = new LogicExprLexer(input); 21 CommonTokenStream tokens = new CommonTokenStream(lexer); 22 23 LogicExprParser parser = new LogicExprParser(tokens); 24 StatContext tree = parser.stat(); 25 26 LogicExprRewriter rewriter = new LogicExprRewriter(invalidKeys); 27 return rewriter.visit(tree); 28 } 29 30 /** 无效的逻辑键集合 */ 31 private final Collection<String> invalidKeys; 32 33 /** 34 * 创建{@link LogicExprRewriter}对象。 35 * 36 * @param invalidKeys 无效的逻辑键集合。 37 */ 38 private LogicExprRewriter(Collection<String> invalidKeys) { 39 this.invalidKeys = invalidKeys; 40 } 41 42 @Override 43 public String visitAnd(AndContext ctx) { 44 String left = visit(ctx.expr(0)); 45 String right = visit(ctx.expr(1)); 46 47 String result; 48 if (left == null) { 49 result = (right == null) ? null : right; 50 } 51 else { 52 if (right == null) { 53 result = left; 54 } 55 else { 56 Interval interval = Interval.of(ctx.expr(0).getStop().getStopIndex() + 1, ctx.expr(1).getStart().getStartIndex() - 1); 57 String and = ctx.getStart().getInputStream().getText(interval); 58 result = left + and + right; 59 } 60 } 61 62 return result; 63 } 64 65 @Override 66 public String visitOr(OrContext ctx) { 67 String left = visit(ctx.expr(0)); 68 String right = visit(ctx.expr(1)); 69 70 String result; 71 if (left == null) { 72 result = (right == null) ? null : right; 73 } 74 else { 75 if (right == null) { 76 result = left; 77 } 78 else { 79 Interval interval = Interval.of(ctx.expr(0).getStop().getStopIndex() + 1, ctx.expr(1).getStart().getStartIndex() - 1); 80 String and = ctx.getStart().getInputStream().getText(interval); 81 result = left + and + right; 82 } 83 } 84 85 return result; 86 } 87 88 @Override 89 public String visitVar(VarContext ctx) { 90 // 去掉左右的括号。例如(a) => a 91 String key = ctx.getText().substring(1, ctx.getText().length() - 1); 92 boolean invalid = isInvalidKey(key); 93 return invalid ? null : ctx.getText(); 94 } 95 96 @Override 97 public String visitGroup(GroupContext ctx) { 98 String expr = visit(ctx.expr()); 99 return (expr == null) ? null : "(" + expr + ")"; 100 } 101 102 private boolean isInvalidKey(String key) { 103 return invalidKeys.contains(key); 104 } 105 106 }
4、最后看看单元测试
1 package com.mycomp.antlr4.le; 2 3 import static org.junit.Assert.assertEquals; 4 import static org.junit.Assert.assertNull; 5 6 import java.util.Arrays; 7 import java.util.Collections; 8 9 import org.junit.Test; 10 11 public class LogicExprRewriterTest { 12 13 @Test 14 public void 无无效键值时_应原样输出() { 15 String le = "(a) and (b) and ((c) or (d) and (e))"; 16 String result = LogicExprRewriter.rewrite(le, Collections.emptyList()); 17 assertEquals(le, result); 18 } 19 20 @Test 21 public void 全部都为无效键值时_应输出null() { 22 String le = "(a) and (b) and ((c) or (d) and (e))"; 23 String result = LogicExprRewriter.rewrite(le, Arrays.asList("a", "b", "c", "d", "e")); 24 assertNull(result); 25 } 26 27 @Test 28 public void 保留空白字符() { 29 String le = "(a) and (b) or (c)"; 30 String result = LogicExprRewriter.rewrite(le, Collections.emptyList()); 31 assertEquals(le, result); 32 } 33 34 @Test 35 public void 对于and操作_如果左边无效_则只返回右边() { 36 String le = "(a) and (b)"; 37 String result = LogicExprRewriter.rewrite(le, Arrays.asList("b")); 38 assertEquals("(a)", result); 39 } 40 41 @Test 42 public void 对于and操作_如果右边无效_则只返回左边() { 43 String le = "(a) and (b)"; 44 String result = LogicExprRewriter.rewrite(le, Arrays.asList("a")); 45 assertEquals("(b)", result); 46 } 47 48 @Test 49 public void 对于or操作_如果左边无效_则只返回右边() { 50 String le = "(a) or (b)"; 51 String result = LogicExprRewriter.rewrite(le, Arrays.asList("b")); 52 assertEquals("(a)", result); 53 } 54 55 @Test 56 public void 对于or操作_如果右边无效_则只返回左边() { 57 String le = "(a) or (b)"; 58 String result = LogicExprRewriter.rewrite(le, Arrays.asList("a")); 59 assertEquals("(b)", result); 60 } 61 62 @Test 63 public void 各类值组合测试() { 64 String le = "(a) and (b) and ((c) or (d) and (e))"; 65 String result; 66 67 result = LogicExprRewriter.rewrite(le, Arrays.asList("c", "e")); 68 assertEquals("(a) and (b) and ((d))", result); 69 70 result = LogicExprRewriter.rewrite(le, Arrays.asList("a", "b", "e")); 71 assertEquals("((c) or (d))", result); 72 } 73 74 }
就这么简单!