背景
项目需要实现前端任意字段匹配查询,故需要配合表达式来生产sql,其实就是定义一种DSL,让前后端相互了解这个语意,调研后决定使用Antlr,相关介绍就不过分多说,直接看效果。语法参考Odata filter ,后期不满足可以直接修改g4文件
Odata filter示例
Country_Region_Code eq 'ES' or Country_Region_Code eq 'US'
Country_Region_Code eq 'ES' and Payment_Terms_Code eq '14 DAYS'
Entry_No ge 610
Entry_No lt 610
VAT_Bus_Posting_Group ne 'EXPORT'
Odata.g4
grammar OData;
/*
* Parser Rules
*/
program: expression;
expression:
LP expression RP # Parenthesis
| K_STARTSWITH LP column=column_name ',' value=TEXT RP # StartsWith
| K_ENDSWITH LP column=column_name ',' value=TEXT RP # EndsWith
| K_CONTAINS LP column=column_name ',' value=TEXT RP # Contains
| column=column_name K_IN LP value=decimal_array RP # InDecimal
| column=column_name K_IN LP value=string_array RP # InText
| column=column_name compare=(
Equal
| NotEqual
| GreaterThan
| GreaterThanOrEqual
| LessThan
| LessThanOrEqual) value=decimal # CompareDecimal
| column=column_name compare=(
Equal
| NotEqual
| GreaterThan
| GreaterThanOrEqual
| LessThan
| LessThanOrEqual) value=TEXT # CompareText
| expression logic = (K_AND | K_OR) expression # Logic
;
column_name
: COLUMN_NAME
| '[' column_name ']'
;
string_array
: TEXT (',' TEXT)*
;
decimal_array
: NUMBER (',' NUMBER)*
;
text: TEXT;
decimal
: NUMBER
;
/*
* Lexer Rules
*/
K_IN: I N;
K_AND: A N D;
K_OR: O R;
K_STARTSWITH: S T A R T S W I T H;
K_ENDSWITH: E N D S W I T H;
K_CONTAINS: C O N T A I N S;
LP : '(';
RP : ')';
Equal: E Q;
NotEqual: N E;
GreaterThan: G T;
GreaterThanOrEqual: G E;
LessThan: L T;
LessThanOrEqual: L E;
COLUMN_NAME
: VALID_ID_START VALID_ID_CHAR*
;
TEXT
:'"' .*? '"'
|'\'' .*? '\''
;
NUMBER
: (SIGN)? UNSIGNED_INTEGER+
| (SIGN)? UNSIGNED_INTEGER+ ('.' UNSIGNED_INTEGER+)?
;
fragment UNSIGNED_INTEGER
: ('0' .. '9')
;
fragment SIGN
: ('+' | '-')
;
fragment VALID_ID_START
: LOWERCASE | UPERCASE | '_'
;
fragment VALID_ID_CHAR
: VALID_ID_START | (LOWERCASE | UPERCASE | DIGIT )
;
fragment LOWERCASE: [a-z];
fragment UPERCASE: [A-Z];
fragment DIGIT: [0-9];
fragment DIGITS: DIGIT+;
fragment A: [aA];
fragment B: [bB];
fragment C: [cC];
fragment D: [dD];
fragment E: [eE];
fragment F: [fF];
fragment G: [gG];
fragment H: [hH];
fragment I: [iI];
fragment J: [jJ];
fragment K: [kK];
fragment L: [lL];
fragment M: [mM];
fragment N: [nN];
fragment O: [oO];
fragment P: [pP];
fragment Q: [qQ];
fragment R: [rR];
fragment S: [sS];
fragment T: [tT];
fragment U: [uU];
fragment V: [vV];
fragment W: [wW];
fragment X: [xX];
fragment Y: [yY];
fragment Z: [zZ];
SPACES: [ \u000B\t\r\n] -> channel(HIDDEN);
前端
UI
npm包
npm i antlr4@4.11.0
Visitor(DSL to js)
// This class defines a complete generic visitor for a parse tree produced by ODataParser.
import ODataParser from "@/project/antlr/ODataParser";
import ODataVisitor from "@/project/antlr/ODataVisitor";
export default class ODataSqlVisitor extends ODataVisitor {
filterPair={
logic:'AND',
pairs:[]
}
getFilterPair(){
return this.filterPair
}
// Visit a parse tree produced by ODataParser#CompareText.
visitCompareText(ctx) {
if (ctx.exception!=null) {
throw ctx.exception;
}
const column = this.visit(ctx.column);
const decimal= ctx.value.text.replace(/^"(.*)"$/,"$1")
const operator = this.getSQLOperator(ctx.compare.type);
const pair= {column ,value: decimal,operator};
this.filterPair.pairs.push(pair)
return pair;
}
// Visit a parse tree produced by ODataParser#CompareDecimal.
visitCompareDecimal(ctx) {
if (ctx.exception!=null) {
throw ctx.exception;
}
const column = this.visit(ctx.column);
const decimal = this.visit(ctx.value);
const operator = this.getSQLOperator(ctx.compare.type);
// return column + " " + operator + " " + decimal;
const pair= {column ,value: decimal,operator};
this.filterPair.pairs.push(pair)
return pair;
}
// Visit a parse tree produced by ODataParser#Logic.
visitLogic(ctx){
if(ctx.exception!=null) {
throw ctx.exception;
}
const left = this.visit(ctx.expression(0));
const right = this.visit(ctx.expression(1));
if (right == null){
return null;
}
if (ctx.logic.type== ODataParser.K_AND) {
this.filterPair.logic = "AND";
// return left + " and " + right;
return null;
}else {
this.filterPair.logic = "OR";
// return left + " or " + right;
return null;
}
}
// Visit a parse tree produced by ODataParser#column_name.
visitColumn_name(ctx) {
return ctx.getText();
}
// Visit a parse tree produced by ODataParser#decimal.
visitDecimal(ctx) {
return ctx.getText();
}
// visitText(ctx) {
// const aa= ctx.getText().replace(/^"(.*)"$/,"$1")
// console.log(aa,ctx.getText())
// debugger
// return aa
// }
getSQLOperator(compare) {
let operator = null;
switch (compare) {
case ODataParser.Equal:
operator = "=";
break;
case ODataParser.NotEqual:
operator = "<>";
break;
case ODataParser.GreaterThan:
operator = ">";
break;
case ODataParser.GreaterThanOrEqual:
operator = ">=";
break;
case ODataParser.LessThan:
operator = "<";
break;
case ODataParser.LessThanOrEqual:
operator = "<=";
break;
}
return operator;
}
}
后端
maven坐标
<!-- https://mvnrepository.com/artifact/org.antlr/antlr4-runtime -->
<dependency>
<groupId>org.antlr</groupId>
<artifactId>antlr4-runtime</artifactId>
<version>4.11.1</version>
</dependency>
Visitor(DSL to sql) 和工具类
package com.sankyu.wms.antlr;
import com.google.common.base.CaseFormat;
public class ODataSqlVisitor extends ODataBaseVisitor {
@Override
public Object visitLogic(ODataParser.LogicContext ctx) {
if (ctx.exception!=null) {
throw ctx.exception;
}
String left = (String) visit(ctx.expression(0));
String right = (String) visit(ctx.expression(1));
if (right == null){
return null;
}
if (ctx.logic.getType()==ODataParser.K_AND) {
return left + " and " + right;
}else {
return left + " or " + right;
}
}
@Override
public Object visitColumn_name(ODataParser.Column_nameContext ctx) {
return CaseFormat.UPPER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, ctx.getText());
}
@Override
public Object visitDecimal(ODataParser.DecimalContext ctx) {
return ctx.getText();
}
// @Override
// public Object visitText(ODataParser.TextContext ctx) {
// return ctx.getText();
// }
@Override
public Object visitCompareDecimal(ODataParser.CompareDecimalContext ctx) {
if (ctx.exception!=null) {
throw ctx.exception;
}
String column = (String)visit(ctx.column);
String decimal = (String)visit(ctx.value);
String operator = getSQLOperator(ctx.compare.getType());
return column + " " + operator + " " + decimal;
}
@Override
public Object visitCompareText(ODataParser.CompareTextContext ctx) {
if (ctx.exception!=null) {
throw ctx.exception;
}
String column = (String)visit(ctx.column);
String decimal = ctx.value.getText();
String operator = getSQLOperator(ctx.compare.getType());
return column + " " + operator + " " + decimal;
}
private String getSQLOperator(Integer compare) {
String operator = null;
switch (compare) {
case ODataParser.Equal:
operator = "=";
break;
case ODataParser.NotEqual:
operator = "<>";
break;
case ODataParser.GreaterThan:
operator = ">";
break;
case ODataParser.GreaterThanOrEqual:
operator = ">=";
break;
case ODataParser.LessThan:
operator = "<";
break;
case ODataParser.LessThanOrEqual:
operator = "<=";
break;
}
return operator;
}
}
参考
- https://olingo.apache.org/doc/odata2/tutorials/Olingo_Tutorial_AdvancedRead_FilterVisitor.html
- https://blog.51cto.com/u_15067225/2603738
- https://learn.microsoft.com/en-us/dynamics-nav/using-filter-expressions-in-odata-uris
- https://github.com/huazailmh/ODataFilterParser/blob/master/Parser/ODataFilterVisitor.cs