Druid_SQL_AST
AST是abstract syntax tree的缩写,也就是抽象语法树。和所有的Parser一样,Druid Parser会生成一个抽象语法树。 本文重点不是讲解AST,想要了解这方面的内容可以参见官方文档: https://github.com/alibaba/druid/wiki/Druid_SQL_AST 本文主要是在接触Druid中发现其中有很多拿来即用的SQL相关工具组件,秉着学习积累的态度于是将这些组件进行整理记录。如果在之后有类似的需要可以拿来即用,或者深入了解后进行扩展开发。
导入依赖:
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.9</version>
</dependency>
SQL解析
通过SQLUtils产生List,通过下面简单的代码就可以解析出SQL语句AST对象
DbType dbType = JdbcConstants.MYSQL;
String sql="select * from dual where 1=1 order by create_time desc limit 10,10";
List<SQLStatement> statementList = SQLUtils.parseStatements(sql, dbType);
for (SQLStatement sqlStatement : statementList) {
System.out.println(sqlStatement);
}
SQL翻译 MySQL语句翻译成Oracle
SQL 翻译,即将一种方言,翻译成另一种。比如输入 MySQL 的 SQL 脚本,使用 MySQL 的 Parser 进行解析,再使用 Oracle 的 Visitor 进行遍历输出,就可以完成 MySQL 脚本到 Oracle 脚本的翻译:
/**
* SQL翻译,将MySQL语句翻译成Oracle
* @author zhangyu
* @date 2022/5/22
**/
@Test
public void test12(){
// MySQL的语言
String sql="select * from dual where 1=1 order by create_time desc limit 10,10";
List<SQLStatement> sqlStatements = SQLUtils.parseStatements(sql, DbType.mysql);
// 将MySQL方言翻译为Oracle的
String oracleSql = SQLUtils.toSQLString(sqlStatements, DbType.oracle);
System.out.println(oracleSql);
}
Tips:上面只演示了MySQL转换Oracle的,其他的类型可以参考DbType这个枚举类。
SQL语句格式化
Druid SQL Parser提供了格式化代码的工具类。这个是基于语义分析做的SQL格式化功能,比其他的SQL格式化做的更智能,效果更好
package com.alibaba.druid.sql;
public class SQLUtils {
String format(String sq, String dbType);
String format(String sq, String dbType, FormatOption option);
}
其中dbType支持mysql/postgresql/odps/oracle/db2/sqlserver option缺省有SQLUtils.DEFAULT_FORMAT_OPTION(大写)、SQLUtils.DEFAULT_LCASE_FORMAT_OPTION(小写)两种可以选择,也可按需要定制化。
/**
* SQL格式化
* @author zhangyu
* @date 2022/5/22
**/
@Test
public void test13(){
String sql = "select t1.id ,t2.name from t1 ,t2 where 1=1 and t1.id=t2.id and t2.name='ZY' order by create_time desc limit 10,10";
String result = SQLUtils.format(sql, JdbcConstants.MYSQL);
System.out.println(result); // 缺省大写格式
System.out.println("-------------------------------------------------------");
String result_lcase = SQLUtils.format(sql
, JdbcConstants.MYSQL
, SQLUtils.DEFAULT_LCASE_FORMAT_OPTION);
System.out.println(result_lcase); // 小写格式
}
SQL语句验证
SQLUtils.parseStatements会根据输入的SQL语句以及方言进行AST语法的分析,我们可以利用这个去判断验证SQL是否是正确的,比较简单的方式就是直接try-catch捕获异常。下面的实例中故意多写了一个where,解析失败
@Test
public void test14(){
String sql = "select t1.id ,t1.name from t1 t2 where ";
try{
List<SQLStatement> statementList = SQLUtils.parseStatements(sql, DbType.mysql);
for (SQLStatement sqlStatement : statementList) {
System.out.println(sqlStatement);
}
}catch (Exception e){
log.error("解析SQL失败",e);
e.printStackTrace();
}
}
获取表名、查询字段、查询条件
SQLUtils.parseStatements会根据输入的SQL语句以及方言进行AST语法的分析,在处理过程中会将SQL语句进行解析分析,并且提供了一些外部的访问方式去获取解析后的信息,结合特定方言的MySqlSchemaStatVisitor对象可以获取这些信息
@Test
public void test15(){
DbType dbType = JdbcConstants.MYSQL;
String sql = "select\n" +
" -- 注释\n" +
" t1.id,\n" +
" t2.name\n" +
"from\n" +
" table1 t1,\n" +
" table2 t2,\n" +
" table3 t3\n" +
"where\n" +
" t1.id = t2.user_id\n" +
" and t3.user_id = t1.id\n" +
" and t1.name = 'zy'\n" +
" and t2.address = '安徽合肥'";
List<SQLStatement> stmtList = SQLUtils.parseStatements(sql, dbType);
for (SQLStatement stmt : stmtList) {
MySqlSchemaStatVisitor visitor = new MySqlSchemaStatVisitor();
stmt.accept(visitor);
System.out.println("获取表名");
Map<TableStat.Name, TableStat> tables = visitor.getTables();
Set<TableStat.Name> tableNameSet = tables.keySet();
for (TableStat.Name name : tableNameSet) {
String tableName = name.getName();
System.out.println(tableName);
}
System.out.println("获取查询列名");
Collection<TableStat.Column> columns = visitor.getColumns();
for (TableStat.Column column : columns) {
if (column.isSelect()) {
String table = column.getTable();
String name = column.getName();
System.out.println(table + "." + name);
}
}
System.out.println("获取查询条件");
List<TableStat.Condition> conditions = visitor.getConditions();
for (TableStat.Condition condition : conditions) {
TableStat.Column column = condition.getColumn();
String table = column.getTable();
String name = column.getName();
String operator = condition.getOperator();
System.out.println();
System.out.print(table + "." + name + operator);
List<Object> values = condition.getValues();
for (Object value : values) {
System.out.print(value);
}
}
}
}
修改表名
所有的AST节点都支持Visitor模式,需要自定义逻辑,可以实现相应的ASTVisitorAdapter派生类,在下面的实例中我们自定义了CustomTableAliasVisitor 然后实现的方法是参数为SQLExprTableSource 。对应接口SQLASTVisitor存在很多空接口,我们可以根据需要修改表名信息,字段信息,关联信息等等,根据需求实现对应的方法。 【实例】在下面的实例中我修改了t2的表名为newTableName。这个应用在很多的场景都适用,比如查询逻辑变动,比如将表改成了视图,再比如分表后的一些查询实现等等
@Test
public void test16() {
DbType dbType = JdbcConstants.MYSQL;
String sql = "select t1.name ,t2.name from t1 ,t2 ";
List<SQLStatement> stmtList = SQLUtils.parseStatements(sql, dbType);
CustomTableAliasVisitor visitor = new CustomTableAliasVisitor();
for (SQLStatement stmt : stmtList) {
stmt.accept(visitor);
}
String sqlString = SQLUtils.toSQLString(stmtList, dbType);
System.out.println(sqlString);
}
static class CustomTableAliasVisitor extends MySqlASTVisitorAdapter {
@Override
public boolean visit(SQLExprTableSource x) {
String alias = x.getAlias();
String tableName = x.getTableName();
if ("t2".equals(tableName)) {
x.setExpr("newTableName");
x.setAlias("t2");
}
return true;
}
}
为SQL增加额外的where条件
对于一些SQL场景下,希望手动添加额外的条件,那么可以通过 SQLUtils.addCondition的方式添加扩展条件
@Test
public void test2() {
String sql = "select * from dual where 1=1 order by create_time desc limit 10,10";
String s = SQLUtils.addCondition(sql, "address = 'AnHui HeFei' ", DbType.mysql);
String s1 = SQLUtils.addCondition(s, "name = 'AnHui' ", SQLBinaryOperator.BooleanOr, false, DbType.mysql);
System.out.println(s1);
}
解析select项、表信息、条件信息
@Test
public void test17() {
String sql = "select t1.name ,t2.address from t1 ,t2 where t1.id=t2.user_id and t1.name =? and t2.name =?";
MySqlStatementParser sqlStatementParser = new MySqlStatementParser(sql);
SQLSelectStatement sqlStatement = (SQLSelectStatement) sqlStatementParser.parseSelect();
SQLSelect sqlSelect = sqlStatement.getSelect();
SQLSelectQueryBlock sqlSelectQuery = (SQLSelectQueryBlock) sqlSelect.getQuery();
StringBuffer out = new StringBuffer();
SQLASTOutputVisitor sqlastOutputVisitor = SQLUtils.createFormatOutputVisitor(out, null, JdbcUtils.MYSQL);
out.delete(0, out.length());
for (SQLSelectItem sqlSelectItem : sqlSelectQuery.getSelectList()) {
if (out.length() > 1) {
out.append(",");
}
sqlSelectItem.accept(sqlastOutputVisitor);
}
System.out.println("查询字段信息 " + out);
out.delete(0, out.length());
sqlSelectQuery.getFrom().accept(sqlastOutputVisitor);
System.out.println("表信息 " + out);
out.delete(0, out.length());
sqlSelectQuery.getWhere().accept(sqlastOutputVisitor);
System.out.println("查询条件信息 " + out);
}
|