应用Druid解析SQL获取查询表字段、参数信息

业务需求:输入一条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。目前对各种数据库的支持如下:

数据库DMLDDL
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<>();


    @Override
    public void endVisit(SQLSelectQueryBlock x) {
        computeCustomColumnBeans();
    }

    @Override
    public boolean visit(SQLExprTableSource sqlExprTableSource) {
        selectTables.add(new CustomTable(sqlExprTableSource));
        return false;
    }

    @Override
    public boolean visit(SQLCharExpr sqlCharExpr ) {
        parserParameter(sqlCharExpr.toString());
        return false;
    }


    @Override
    public boolean visit(SQLSelectItem sqlSelectItem) {
        selectItems.add(sqlSelectItem );
        return false;
    }

    @Override
    public boolean visit(SQLVariantRefExpr sqlVariantRefExpr) {
        parserParameter(sqlVariantRefExpr.getName());
        return false;
    }


    @Override
    public 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);
  • 7
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值