业务需求:输入一条SQL,解析出对应的结果表字段列表与参数列表。
select t.id,t.name,date_format(update_time,'%Y-%m') as update_month
from t_user t
where t.account=#{account:varchar};
解析后需获得:
- 结果字段信息:id,name,update_month
- 涉及的表:t_user(别名>t)
- 参数信息:account
- 格式化处理后的SQL语句: select t.id,t.name,date_format(update_time,‘%Y-%m’) as update_month from t_user t where t.account=?;
- …
因为输入的SQL复杂度不一,可能存在子查询和连表查询,所以想借用轮子,再自己定制需要的部分。调研了几个SQL Parser,结合当前项目,还是选择了开源的druid sql parser。
Druid是Java语言中最好的数据库连接池。提供强大的监控和扩展功能。其中,SQL Parser是Druid的一个重要组成部分,可用来实现防御SQL注入(WallFilter)、合并统计没有参数化的SQL(StatFilter的mergeSql)、SQL格式化、分库分表。
https://github.com/alibaba/druid
https://github.com/alibaba/druid/wiki/SQL-Parser
Druid的sql parser是目前支持各种数据语法最完备的SQL Parser。目前对各种数据库的支持如下:
数据库 | DML | DDL |
---|---|---|
odps | 完全支持 | 完全支持 |
mysql | 完全支持 | 完全支持 |
postgresql | 完全支持 | 完全支持 |
oracle | 支持大部分 | 支持大部分 |
sql server | 支持常用的 | 支持常用的ddl |
db2 | 支持常用的 | 支持常用的ddl |
hive | 支持常用的 | 支持常用的ddl |
对于本文中的业务场景,是支持。
Druid SQL Parser分三个模块:Parser、AST、Visitor。Parser将SQL文本解析为AST语法树,Visitor有遍历AST的能力。
在本文中因为输入的是相对规范的sql语法,Parser可以完全解析为需要的语法树。所以后续只需要自定义Visitor来获取需要的信息。
其中有提供SQLASTParameterizedVisitor(重点在:参数等)、SchemaStatVisitor(重点在:数据表关系、排序、分组等)等来获取AST的信息。因为没有整合开头所需的内容数据,所以本文自定义了一个。
注意:不要直接继承SQLASTParameterizedVisitor、SchemaStatVisitor这些已经实现SQLASTVisitor接口的类,因为会导致执行顺序偏差,反正在测试时,发现参数解析出现了丢失情况。
解析获得的结果类:
@Data
public class CustomSQLParser {/*** 格式化SQL语句*/private String formatSql;/*** 预处理后SQL语句*/private String prepareSql;/*** 涉及的表信息 tableName、tableAlias*/private List<CustomTable> tables;/*** 查询获得的结果字段信息 column、columnAlias、dataType*/private List<CustomColumn> columns;/*** 参数信息 paramName、paramType、expr、operation*/private List<CustomParameter> parameters;/*** 参数名称*/private List<String> parameterNames;
}
自定义Visitor
@Data
public class CustomSQLASTVisitor implements SQLASTVisitor {public static final Pattern PARAMETER_PATTERN = Pattern.compile("[#\\$]\\{(\\w+)(?::(\\w+))?\\}");protected List<SQLSelectItem> selectItems = new ArrayList<>();protected List<CustomTable> selectTables = new ArrayList<>();protected List<CustomColumn> selectColumns = new ArrayList<>();protected List<String> parameterNames = new ArrayList<>();protected List<CustomParameter> parameters = new ArrayList<>();protected List<String> columnNames = new ArrayList<>();protected Map<String, String> opera = new HashMap<>();@Overridepublic void endVisit(SQLSelectQueryBlock x) {computeCustomColumnBeans();}@Overridepublic boolean visit(SQLExprTableSource sqlExprTableSource) {selectTables.add(new CustomTable(sqlExprTableSource));return false;}@Overridepublic boolean visit(SQLCharExpr sqlCharExpr ) {parserParameter(sqlCharExpr.toString());return false;}@Overridepublic boolean visit(SQLSelectItem sqlSelectItem) {selectItems.add(sqlSelectItem );return false;}@Overridepublic boolean visit(SQLVariantRefExpr sqlVariantRefExpr) {parserParameter(sqlVariantRefExpr.getName());return false;}@Overridepublic void endVisit(SQLBinaryOpExpr sqlBinaryOpExpr) {parserWhereParam(sqlBinaryOpExpr);this.parameters.forEach(p->{if(opera.containsKey(p.getExpr())){p.setOperation(opera.get(p.getExpr()));}});}protected void parserParameter(String expr) {Matcher matcher = PARAMETER_PATTERN.matcher(expr);if (matcher.find()) {String field = matcher.group(1);String fieldType = matcher.group(2);parameterNames.add(field);parameters.add(new CustomParameter(field, fieldType, expr));}}protected void computeCustomColumnBeans() {selectItems.forEach(item -> {String alias = item.getAlias();CustomColumn curColumn = null;if (item.getExpr() instanceof SQLIdentifierExpr) {SQLIdentifierExpr expr = (SQLIdentifierExpr) item.getExpr();curColumn = new CustomColumn(selectTables.get(0).getTableName(), selectTables.get(0).getTableAlias(), expr.getName(), alias);} else if (item.getExpr() instanceof SQLAllColumnExpr) {SQLAllColumnExpr expr = (SQLAllColumnExpr) item.getExpr();curColumn = new CustomColumn(selectTables.get(0).getTableName(), selectTables.get(0).getTableAlias(), expr.toString(), alias);} else if (item.getExpr() instanceof SQLMethodInvokeExpr) {SQLMethodInvokeExpr expr = (SQLMethodInvokeExpr) item.getExpr();curColumn = new CustomColumn(selectTables.get(0).getTableName(), selectTables.get(0).getTableAlias(), expr.toString(), alias);} else if (item.getExpr() instanceof SQLPropertyExpr) {SQLPropertyExpr expr = (SQLPropertyExpr) item.getExpr();curColumn = new CustomColumn(getTableNameByAlias(expr.getOwnerName()), expr.getOwnerName(), expr.getName(), item.getAlias());}if (null != curColumn) {selectColumns.add(curColumn);columnNames.add(!StringUtils.isNullOrEmpty(curColumn.getColumnAlias()) ? curColumn.getColumnAlias() : curColumn.getColumn());}});}/*** 根据查询表别名获取查询表名** @param alias 查询表别名* @return 查询表名*/protected String getTableNameByAlias(String alias) {return getTableByAlias(alias).map(CustomTable::getTableName).orElse(null);}/*** 根据查询表别名获取查询表** @param alias 查询表别名* @return 查询表*/protected Optional<CustomTable> getTableByAlias(String alias) {return selectTables.stream().filter(table -> alias.equals(table.getTableAlias())).findFirst();}/*** 解析Where中的参数** @param sqlBinaryOpExpr*/protected void parserWhereParam(SQLBinaryOpExpr sqlBinaryOpExpr) {SQLExpr right = sqlBinaryOpExpr.getRight();SQLExpr left = sqlBinaryOpExpr.getLeft();if (left instanceof SQLIdentifierExpr || left instanceof SQLVariantRefExpr|| left instanceof SQLPropertyExpr) {Matcher matcher = PARAMETER_PATTERN.matcher(sqlBinaryOpExpr.toString());if (matcher.find()) {String expr = matcher.group();opera.put(expr, sqlBinaryOpExpr.getOperator().getName());}} else if(left instanceof SQLBinaryOpExpr){Matcher matcherRight = PARAMETER_PATTERN.matcher(right.toString());if (matcherRight.find()) {String expr = matcherRight.group();opera.put(expr, ((SQLBinaryOpExpr) right).getOperator().getName());}SQLExpr leftSubRight = ((SQLBinaryOpExpr) left).getRight();// 解析右边System.out.println("leftSubRight.toString():" + leftSubRight.toString());Matcher matcherLeftSubRight = PARAMETER_PATTERN.matcher(leftSubRight.toString());if (matcherLeftSubRight.find()) {String expr = matcherLeftSubRight.group();opera.put(expr, ((SQLBinaryOpExpr) leftSubRight).getOperator().getName());}SQLExpr leftSubLeft = ((SQLBinaryOpExpr) left).getLeft();if (leftSubLeft instanceof SQLBinaryOpExpr) {parserWhereParam((SQLBinaryOpExpr) leftSubLeft);}Matcher matcherLeftSubLeft = PARAMETER_PATTERN.matcher(leftSubLeft.toString());if (matcherLeftSubLeft.find()) {String expr = matcherLeftSubLeft.group();opera.put(expr, ((SQLBinaryOpExpr) leftSubLeft).getOperator().getName());}}}
}
使用
SQLStatement statement = SQLUtils.parseSingleStatement(sql, DbType.mysql);
CustomSQLASTVisitor visitor = new CustomSQLASTVisitor();statement.accept(visitor);