动态SQL解析

本文涉及的产品
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,高可用系列 2核4GB
简介: `动态sql`的一大难点就是根据参数动态生成最终查询sql,我们熟知的orm框架`mybatis`为了实现动态sql能力,引入大量的标签,功能强大的同时也带来使用的复杂性,至于`mybatis-plus`是怎么实现的,大家自行查阅资料,我想复杂性并不低。当然我这里想说一下,全方面完美地实现动态sql本身就是一件复杂的事情。

1.背景

动态sql的一大难点就是根据参数动态生成最终查询sql,我们熟知的orm框架mybatis为了实现动态sql能力,引入大量的标签,功能强大的同时也带来使用的复杂性,至于mybatis-plus是怎么实现的,大家自行查阅资料,我想复杂性并不低。当然我这里想说一下,全方面完美地实现动态sql本身就是一件复杂的事情。

接下来我想讲讲我的思路,借助于阿里开源的Druid在sql语法解析上的能力,根据传递参数动态判断实现sql语法where条件语句的动态剔除,从而达到动态sql能力。所以在讲述动态sql解析之前我们先来了解一下druid怎么解析sql语法的。

项目推荐:基于SpringBoot2.x、SpringCloud和SpringCloudAlibaba企业级系统架构底层框架封装,解决业务开发时常见的非功能性需求,防止重复造轮子,方便业务快速开发和企业技术栈框架统一管理。引入组件化的思想实现高内聚低耦合并且高度可配置化,做到可插拔。严格控制包依赖和统一版本管理,做到最少化依赖。注重代码规范和注释,非常适合个人学习和企业使用

Github地址https://github.com/plasticene/plasticene-boot-starter-parent

Gitee地址https://gitee.com/plasticene3/plasticene-boot-starter-parent

微信公众号Shepherd进阶笔记

交流探讨群:Shepherd_126

2.Druid_SQL_AST

SQL Parser是Druid的一个重要组成部分,Druid内置使用SQL Parser来实现防御SQL注入(WallFilter)、合并统计没有参数化的SQL(StatFilter的mergeSql)、SQL格式化、分库分表。

和另一个知名解析器Antlr生成的SQL有很大不同的是,Druid SQL Parser性能非常好,可以用于生产环境直接对SQL进行分析处理

AST是abstract syntax tree的缩写,也就是抽象语法树。和所有的Parser一样,Druid Parser会生成一个抽象语法树。

2.1 AST节点类型

在Druid中,AST节点类型主要包括SQLObject、SQLExpr、SQLStatement三种抽象类型 ,可以说这三个类型贯穿了整个sql语法树解析,提炼关键信息(eg:查询的字段、表名、where条件等等)过程

package com.alibaba.druid.sql.ast;

interface SQLObject {
   }
interface SQLExpr extends SQLObject {
   }
interface SQLStatement extends SQLObject {
   }

interface SQLTableSource extends SQLObject {
   }
class SQLSelect extends SQLObject {
   }
class SQLSelectQueryBlock extends SQLObject {
   }

2.2 常见SQLExpr类型

SQLExpr抽象类型对动态sql解析非常重要,动态sql的解析核心就是需要提炼出sql的所有where条件语句,然后对每一个条件语句结合参数进行动态处理,这里提炼出的每一个where条件就是下面常见类型中的一个

package com.alibaba.druid.sql.ast.expr;

// SQLName是一种的SQLExpr的Expr,包括SQLIdentifierExpr、SQLPropertyExpr等
public interface SQLName extends SQLExpr {
   }

// 例如 ID = 3 这里的ID是一个SQLIdentifierExpr
class SQLIdentifierExpr implements SQLExpr, SQLName {
   
    String name;
} 

// 例如 A.ID = 3 这里的A.ID是一个SQLPropertyExpr
class SQLPropertyExpr implements SQLExpr, SQLName {
   
    SQLExpr owner;
    String name;
} 

// 例如 ID = 3 这是一个SQLBinaryOpExpr
// left是ID (SQLIdentifierExpr)
// right是3 (SQLIntegerExpr)
class SQLBinaryOpExpr implements SQLExpr {
   
    SQLExpr left;
    SQLExpr right;
    SQLBinaryOperator operator;
}

// 例如 select * from where id = ?,这里的?是一个SQLVariantRefExpr,name是'?'
class SQLVariantRefExpr extends SQLExprImpl {
    
    String name;
}

// 例如 ID = 3 这里的3是一个SQLIntegerExpr
public class SQLIntegerExpr extends SQLNumericLiteralExpr implements SQLValuableExpr {
    
    Number number;

    // 所有实现了SQLValuableExpr接口的SQLExpr都可以直接调用这个方法求值
    @Override
    public Object getValue() {
   
        return this.number;
    }
}

// 例如 NAME = 'jobs' 这里的'jobs'是一个SQLCharExpr
public class SQLCharExpr extends SQLTextLiteralExpr implements SQLValuableExpr{
   
    String text;
}

2.3. 常用的SQLStatemment

最常用的Statement当然是SELECT/UPDATE/DELETE/INSERT,他们分别是

package com.alibaba.druid.sql.ast.statement;

class SQLSelectStatement implements SQLStatement {
   
    SQLSelect select;
}
class SQLUpdateStatement implements SQLStatement {
   
    SQLExprTableSource tableSource;
     List<SQLUpdateSetItem> items;
     SQLExpr where;
}
class SQLDeleteStatement implements SQLStatement {
   
    SQLTableSource tableSource; 
    SQLExpr where;
}
class SQLInsertStatement implements SQLStatement {
   
    SQLExprTableSource tableSource;
    List<SQLExpr> columns;
    SQLSelect query;
}

2.4. SQLTableSource

常见的SQLTableSource包括SQLExprTableSource、SQLJoinTableSource、SQLSubqueryTableSource、SQLWithSubqueryClause.Entry

class SQLTableSourceImpl extends SQLObjectImpl implements SQLTableSource {
    
    String alias;
}

// 例如 select * from emp where i = 3,这里的from emp是一个SQLExprTableSource
// 其中expr是一个name=emp的SQLIdentifierExpr
class SQLExprTableSource extends SQLTableSourceImpl {
   
    SQLExpr expr;
}

// 例如 select * from emp e inner join org o on e.org_id = o.id
// 其中left 'emp e' 是一个SQLExprTableSource,right 'org o'也是一个SQLExprTableSource
// condition 'e.org_id = o.id'是一个SQLBinaryOpExpr
class SQLJoinTableSource extends SQLTableSourceImpl {
   
    SQLTableSource left;
    SQLTableSource right;
    JoinType joinType; // INNER_JOIN/CROSS_JOIN/LEFT_OUTER_JOIN/RIGHT_OUTER_JOIN/...
    SQLExpr condition;
}

// 例如 select * from (select * from temp) a,这里第一层from(...)是一个SQLSubqueryTableSource
SQLSubqueryTableSource extends SQLTableSourceImpl {
   
    SQLSelect select;
}

/* 
例如
WITH RECURSIVE ancestors AS (
    SELECT *
    FROM org
    UNION
    SELECT f.*
    FROM org f, ancestors a
    WHERE f.id = a.parent_id
)
SELECT *
FROM ancestors;

这里的ancestors AS (...) 是一个SQLWithSubqueryClause.Entry
*/
class SQLWithSubqueryClause {
   
    static class Entry extends SQLTableSourceImpl {
    
         SQLSelect subQuery;
    }
}

2.5. SQLSelect & SQLSelectQuery

SQLSelectStatement包含一个SQLSelect,SQLSelect包含一个SQLSelectQuery,都是组成的关系。SQLSelectQuery有主要的两个派生类,分别是SQLSelectQueryBlock和SQLUnionQuery。

class SQLSelect extends SQLObjectImpl {
    
    SQLWithSubqueryClause withSubQuery;
    SQLSelectQuery query;
}

interface SQLSelectQuery extends SQLObject {
   }

class SQLSelectQueryBlock implements SQLSelectQuery {
   
    List<SQLSelectItem> selectList;
    SQLTableSource from;
    SQLExprTableSource into;
    SQLExpr where;
    SQLSelectGroupByClause groupBy;
    SQLOrderBy orderBy;
    SQLLimit limit;
}

class SQLUnionQuery implements SQLSelectQuery {
   
    SQLSelectQuery left;
    SQLSelectQuery right;
    SQLUnionOperator operator; // UNION/UNION_ALL/MINUS/INTERSECT
}

2.6 Visitor

Druid提供了多种默认实现的Visitor,可以满足基本需求,如果默认提供的不满足需求,可自行实现自定义Visitor。

比如我们要统计下一条SQL中涉及了哪些表 ,如果我们不用visitor,自行遍历AST,能实现,但是很繁琐,使用visitor就很简单:

public String parseSQL(String sql) {
   
    // 新建 MySQL Parser
    SQLStatementParser parser = SQLParserUtils.createSQLStatementParser(sql, JdbcUtils.MYSQL);

    // 使用Parser解析生成AST,这里SQLStatement就是AST
    SQLSelectStatement statement = (SQLSelectStatement) parser.parseStatement();

    // 使用visitor来访问AST
    MySqlSchemaStatVisitor visitor = new MySqlSchemaStatVisitor();
    statement.accept(visitor);
    // 解析sql
    parseSelect(statement);

    // sql中的所有表
    Map<TableStat.Name, TableStat> tables = visitor.getTables();

    // druid格式化之后的sql
    String beautySQL = SQLUtils.toSQLString(statement);

    return beautySQL;
}

以上类型是动态sql解析都需要用到的,当然druid不止有这些类型,这里不在详情阐述,想要了解可自行到官网查看

3 使用Druid怎么实现动态sql解析

3.1 解析示例

在讲述实现原理之前直接上测试用例

SELECT
    * 
FROM
    (
    SELECT
        id,
        org_id,
        NAME,
        age,
        phone,
        email,
        ( SELECT dep_name FROM dept WHERE dep_id = @depId ) 
    FROM
        USER t 
    WHERE
        t.is_delete = 0 
        AND t.id IN ( SELECT id FROM USER WHERE org_id = @orgId ) 
        AND create_time >= @createTime 
        AND age = @age 
        AND type IN @type 
        AND state <> @state 
        AND ( name = @name OR user_name = @name ) 
    ) AS a 
WHERE
    a.org_id = @orgId 
    AND id IN @id 
    AND email LIKE '%@163.com' 
    AND phone = @phone

这里我们使用@var代表动态参数,同时sql语句@就是用来定义一个变量的,这点不谋而合。

测试用例代码如下:


 public static void main(String[] args) {
   
        String sql = "SELECT * FROM (SELECT id, org_id, NAME, age, phone, email, ( SELECT dep_name FROM dept   WHERE dep_id = @depId ) FROM USER t \n" +
                "WHERE t.is_delete = 0 AND t.id IN ( SELECT id FROM USER WHERE org_id = @orgId ) AND create_time>= @createTime AND age = @age AND type IN @type AND state <> @state \n" +
                "AND ( name = @name OR user_name = @name ) ) AS a \n" +
                "WHERE a.org_id = @orgId AND id IN @id AND email LIKE '%@163.com' AND phone = @phone";
        Map<String, String> params = new HashMap<>();
        params.put("@depId", "567");
        params.put("@orgId", "432");
        params.put("@createTime", "'2022-06-30 19:00:00'");
        params.put("@type", "6,7,8,9,10000");
        params.put("@name", "'张三'");
        params.put("@phone", "'1234567789'");
        DynamicSqlParser dynamicSqlParser = new DynamicSqlParser();
        String beautySQL = dynamicSqlParser.parseSQL(sql, params);
        System.out.println(beautySQL);
    }

从上面可以看到我们只传了部分参数,执行结果如下:

SELECT *
FROM (
    SELECT id, org_id, NAME, age, phone
        , email
        , (
            SELECT dep_name
            FROM dept
            WHERE dep_id = 567
        )
    FROM USER t
    WHERE t.is_delete = 0
        AND t.id IN (
            SELECT id
            FROM USER
            WHERE org_id = 432
        )
        AND create_TIME >= '2022-06-30 19:00:00'
        AND 1 = 1
        AND type IN (6,7,8,9,10000)
        AND 1 = 1
        AND (name = '张三'
            OR user_name = '张三')
) a
WHERE a.org_id = 432
    AND 1 = 1
    AND email LIKE '%@163.com'
    AND phone = '1234567789'

可以看到,我们传的参数都正常匹配上了对应的条件中,没传的参数对应的条件全部被替换成了1=1,这样就可以做到想查什么就查什么的动态sql能力了。

3.2 实现原理

实现思路相对来说还是挺简单的,借助Druid解析sql的能力,提炼出sql里面所有的带@var动态参数的where条件:

    /**
     * 存放解析出来的带@var动态参数的条件语句 动态参数 → 条件set集合
     */
    public static final ThreadLocal<Map<String, Set<String>>> varToWhere = new ThreadLocal<>();

    private void setVarToWhere(String var, String where) {
   
    Map<String, Set<String>> map = varToWhere.get();
    if (map == null) {
   
    map = new HashMap<>();
    }
    Set<String> wheres = map.get(var);
    if (wheres == null) {
   
    wheres = new HashSet<>();
    }
    wheres.add(where);
    map.put(var, wheres);
    varToWhere.set(map);
  }

为什么动态参数是对应一个条件set集合呢?

因为sql语句一个动态参数@var有可能用在了不同地方,如上面的@orgId参数用在了org_id = @orgId和a.org_id = @orgId这两个where条件中,所以我们要对应一个set

通过debug上面测试用例可以看到:varToWhere变量存的值:

{
  @createTime=[create_TIME >= @createTime], 
  @age=[age = @age], 
  @state=[state <> @state],
  @name=[user_name = @name, name = @name],
  @type=[type IN (@type)], 
  @orgId=[org_id = @orgId, a.org_id = @orgId], 
  @id=[id IN (@id)],
  @depId=[dep_id = @depId], 
  @phone=[phone = @phone]
}

最后经过sql文本字符串替换出来得到最终上面示例的sql语句

    public String parseSQL(String sql, Map<String, String> params) {
        String beautySQL = parseSQL(sql);
        Map<String, Set<String>> map = varToWhere.get();
        System.out.println(map);
        varToWhere.remove();
        for (Map.Entry<String, Set<String>> entry : map.entrySet()) {
            String var = entry.getKey();
            Set<String> wheres = entry.getValue();
            String replaceWhere = null;
            for(String where : wheres) {
                // 没有传这个参数,那么把这个条件变成1=1
                if (!params.containsKey(var)) {
                    replaceWhere = CONSTANT_CONDITION;
                } else {
                    replaceWhere = where.replaceAll(var, params.get(var));
                }
                beautySQL = beautySQL.replace(where, replaceWhere);
            }
        }
        return beautySQL;
    }

经个人测试,动态sql解析通过上面方法相对稳定,没出现问题,当然由于sql的千奇百怪,并一定保证所有sql场景都能正常解析,欢迎大家提供复杂sql测试。

目录
相关文章
|
12天前
|
SQL 监控 安全
员工上网行为监控软件:SQL 在数据查询监控中的应用解析
在数字化办公环境中,员工上网行为监控软件对企业网络安全和管理至关重要。通过 SQL 查询和分析数据库中的数据,企业可以精准了解员工的上网行为,包括基础查询、复杂条件查询、数据统计与分析等,从而提高网络管理和安全防护的效率。
25 0
|
1月前
|
SQL 监控 数据库
SQL语句是否都需要解析及其相关技巧和方法
在数据库管理中,SQL(结构化查询语言)语句的使用无处不在,它们负责数据的查询、插入、更新和删除等操作
|
1月前
|
SQL 存储 数据库
SQL语句是否都需要解析及其相关技巧与方法
在数据库管理系统中,SQL(Structured Query Language)语句作为与数据库交互的桥梁,其执行过程往往涉及到一个或多个解析阶段
|
1月前
|
SQL 数据可视化 BI
SQL语句及查询结果解析:技巧与方法
在数据库管理和数据分析中,SQL语句扮演着至关重要的角色
|
1月前
|
SQL 监控 关系型数据库
SQL错误代码1303解析与处理方法
在SQL编程和数据库管理中,遇到错误代码是常有的事,其中错误代码1303在不同数据库系统中可能代表不同的含义
|
1月前
|
SQL 存储 关系型数据库
SQL默认索引是什么:深入解析与技巧
在SQL数据库中,索引是一种用于提高查询性能的重要数据结构
|
1月前
|
SQL 开发框架 .NET
ASP.NET连接SQL数据库:实现过程与关键细节解析an3.021-6232.com
随着互联网技术的快速发展,ASP.NET作为一种广泛使用的服务器端开发技术,其与数据库的交互操作成为了应用开发中的重要环节。本文将详细介绍在ASP.NET中如何连接SQL数据库,包括连接的基本概念、实现步骤、关键代码示例以及常见问题的解决方案。由于篇幅限制,本文不能保证达到完整的2000字,但会确保
|
SQL Oracle 关系型数据库
动态sql 和静态sql 上篇
本文讲解基于Oracle高性能动态SQL程序开发。 1.静态SQL与动态SQL: Oracle编译PL/SQL程序块分为两个种: 其一为前期联编(early binding),即SQL语句在程序编译期间就已经确定,大多数的编译情况属于这种类型; 另外一种是后期联编(late binding),即SQL语句只有在运行阶段才能建立,例如当查询条件为用户输入时,那么Oracle的SQL引擎就无法在编译期对该程序语句进行确定,只能在用户输入一定的查询条件后才能提交给SQL引擎进行处理。
1156 0
|
存储 SQL 关系型数据库
动态sql 和静态sql 下篇
1、批量绑定让oracle 在对一组值进行sql操作时绑定一个变量,这一组数据可以是任何的plsql的聚合类型(联合数组,内嵌表,可变数组),集合元素必须是sql的数据类型,比如char date mumber。
1128 0
|
2月前
|
关系型数据库 MySQL 网络安全
5-10Can't connect to MySQL server on 'sh-cynosl-grp-fcs50xoa.sql.tencentcdb.com' (110)")
5-10Can't connect to MySQL server on 'sh-cynosl-grp-fcs50xoa.sql.tencentcdb.com' (110)")

推荐镜像

更多