Bootstrap

Sql解析转换之JSqlParse完整介绍

1、 jsqlparse介绍

JSqlParse是一款很精简的sql解析工具,它可以将常用的sql文本解析成具有层级结构的“语法树”,我们可以针对解析后的“树节点(也即官网里说的有层次结构的java类)”进行处理进而生成符合我们要求的sql形式。

官网给的介绍很简洁:JSqlParser 解析 SQL 语句并将其转换为 Java 类的层次结构。生成的层次结构可以使用访问者模式进行访问(官网地址:JSqlParser - Home)。

官网的介绍即是该中间件的全部,虽然介绍很短,但是其功能着实强悍。

2、jar包结构介绍

这里我使用的是4.3版本,maven依赖如下:

<dependency>
    <groupId>com.github.jsqlparser</groupId>
    <artifactId>jsqlparser</artifactId>
    <version>4.3</version>
</dependency>

JSqlParse的总体代码量不大,结构也很简单,其项目整体结构图如下:

 可以看到其总共只有五个大的包,各个包的功能定义也很清晰:
expression:包含表达式相关的类和接口,可以简单看做sql解析后的组成对象之一。如果需要对sql进行一些更改变换,基本都会涉及到这个包。
parse:JSqlParse最核心的包,这个包里的类实现了sql的解析,进而我们才可以对解析后的sql(“java类”)做各种自定义处理。虽然这个包是最核心的包,但如果纯粹从使用角度上来说可以不必太在意它,除非我们想深入了解sql解析的过程。
schema:可以理解为模式,即定义一些和数据中概念相对应的类,如表Table、列Column等。
statement:sql语句也分很多种,如增删改查等,这个包下就对应各种解析后java类所组成的sql语句,其内部结构如下:

util:JSqlParse解析中用到的工具类,基本也不用太在意,不过有个TablesNamesFinder类则具有较强的参考价值。

其中该组件最牛逼的地方是parse包的解析,即将sql解析成一组有血缘(或者成层级嵌套)的对象集,要了解这块,需要对antlr有较深的理解才行。感兴趣的可以专门去看一下。不过如果我们只是使用,就不需要专门了解语法的解析了,我们只需要知道如何对解析后的sql进行修改即可。下面我会先讲解大致大体的如何去做,最后一节再讲解其中的一些原理。

3、使用介绍

sql语句的修改是通过实现对应的访问者接口实现的,比如你想对from之后的table名称进行处理,那么你只需要实现 FromItemVisitor 接口并重写 访问Table的方法即可。如果你想对sql中的函数进行处理,那么你只需要实现ExpressionVisitor接口并重写其中对应的方法接口即可。

是不是很简单,不过这里有个问题就是我们如何把我们自定义的访问者传给解析后的sql对象。因为解析后的sql对象是具有层级的,我们要处理的对象很有可能在最内层。如果你想自己遍历解析后的sql对象,然后把访问者传给特定的对象,这个方法虽然可行,但只能用于于不包含嵌套或者嵌套层次不深的sql语句,一旦包含嵌套语句或者sql语句很复杂,你很难一层层的去处理。

正确的做法是从sql解析后的第一层开始,将每个遇到的相关访问者接口都实现一遍,这样在获得解析后的sql对象后,直接就可以将自定义访问者对象传进去,也不需要我们自己一层层去剥开sql对象。我们只需要专注于自己需要的重写的访问者方法即可。展示下我实际中变更select语句用到的一些访问者接口,贴出来给大家看下:

        StatementVisitor, SelectVisitor, SelectItemVisitor, FromItemVisitor, GroupByVisitor,         ExpressionVisitor,ItemsListVisitor

这些访问者接口我也不是一次性全实现的,而是从最外层的StatementVisitor开始,一点点加的,后续如果有需要可能还会再加,这个过程是一个比较繁琐的逐渐深入和查漏补缺的过程,所以在sql语法替换时一定要保持谨慎。但这也给出一个建议,千万不要试图追踪各个模块的迭代处理
情况,这样很容易把你绕进去,你只需关注当前所在的模块即可,其它的通过accpet交给其它对应的visitor去处理。

下面以更改select类型语句,将from之后table表名称从table1改为table2,和将max函数修改为min函数作为目标,我们来实现下这个需求:

首先是流程代码,如下:

public class Main {
    
    public static void main(String[] args) throws Exception{
        //1、获取原始sql输入
        String sql = "select max(age) from table1";
        System.out.println("old sql:[{}]"+sql);
        //2、创建解析器
        CCJSqlParserManager mgr = new CCJSqlParserManager();
        //3、使用解析器解析sql生成具有层次结构的java类
        Statement stmt = mgr.parse(new StringReader(sql));
        //4、将自定义访问者传入解析后的sql对象
        stmt.accept(new MyJSqlVisitor());
        //5、打印转换后的sql语句
        System.out.println("new sql:[{}]" + stmt.toString());
    }
    
}

其次是最核心的访问者接口实现类,这里为了便于向大家展示sql修改的过程,我们一个个的添加接口:

首先是stmt.accept,这个对象接收的是一个StatementVisitor,所以我们在自定义的类MyJSqlVisitor中先实现这个接口,因为我们要改的是select类语句,所以我们可以找到对应的visitor方法(至于为什么这个接口就是跟selet语句相关,一个是根据方法名推断,一个是debug查看,debug可以看到sql语句一层层的对象,再细就不啰嗦了,实战个几次就懂了)

public class MyJSqlVisitor implements StatementVisitor {
    
    @Override
    public void visit(Select select) {
        SelectBody selectBody = select.getSelectBody();
        if (selectBody != null) {
            selectBody.accept(this);
        }
    }

}

注意下,这里我只列出了一个实现的方法,是因为篇幅有限,我只截取了实现改动的方法,后续也是只展示实现了变动的代码,接着可以看到selectBody也需要一个SelectVisitor类型的访问者,所以我们再MyJSqlVisitor中添加实现该接口:

public class MyJSqlVisitor implements StatementVisitor, SelectVisitor {
    
    @Override
    public void visit(Select select) {
        SelectBody selectBody = select.getSelectBody();
        if (selectBody != null) {
            selectBody.accept(this);
        }
    }

    @Override
    public void visit(PlainSelect plainSelect) {
        /** 处理select字段 */
        List<SelectItem> selectItems = plainSelect.getSelectItems();
        if (selectItems != null && selectItems.size() > 0) {
            selectItems.forEach(selectItem -> {
                selectItem.accept(this);
            });
        }

        /** 处理表名或子查询 */
        FromItem fromItem = plainSelect.getFromItem();
        if (fromItem!=null){
            fromItem.accept(this);
        }
    }

}

该接口对应的visit方法中 selectItem和fromItem同时还需要SelectItemVisitor,FromItemVisitor两种访问者,所以我们先来实现SelectItemVisitor这个接口:

public class MyJSqlVisitor implements StatementVisitor, SelectVisitor ,SelectItemVisitor {
    
    @Override
    public void visit(Select select) {
        SelectBody selectBody = select.getSelectBody();
        if (selectBody != null) {
            selectBody.accept(this);
        }
    }

    @Override
    public void visit(PlainSelect plainSelect) {
        /** 处理select字段 */
        List<SelectItem> selectItems = plainSelect.getSelectItems();
        if (selectItems != null && selectItems.size() > 0) {
            selectItems.forEach(selectItem -> {
                selectItem.accept(this);
            });
        }

        /** 处理表名或子查询 */
        FromItem fromItem = plainSelect.getFromItem();
        if (fromItem!=null){
            fromItem.accept(this);
        }
    }

    // 这个方法我们并没有考虑完全,比如select项目中可能有子查询还有可能有case表达式,这些我们都没考虑,这里只是先展示了一种思路。
    @Override
    public void visit(SelectExpressionItem selectExpressionItem) {
        if (Function.class.isInstance(selectExpressionItem.getExpression())) {
            Function function = (Function) selectExpressionItem.getExpression();
            function.accept(this);
        }
    }

}

可以看到function.accept还需要一个ExpressionVisitor,这里我们接着实现它:

public class MyJSqlVisitor implements StatementVisitor, SelectVisitor ,SelectItemVisitor, ExpressionVisitor {
    
    @Override
    public void visit(Select select) {
        SelectBody selectBody = select.getSelectBody();
        if (selectBody != null) {
            selectBody.accept(this);
        }
    }

    @Override
    public void visit(PlainSelect plainSelect) {
        /** 处理select字段 */
        List<SelectItem> selectItems = plainSelect.getSelectItems();
        if (selectItems != null && selectItems.size() > 0) {
            selectItems.forEach(selectItem -> {
                selectItem.accept(this);
            });
        }

        /** 处理表名或子查询 */
        FromItem fromItem = plainSelect.getFromItem();
        if (fromItem!=null){
            fromItem.accept(this);
        }
    }

    // 这个方法我们并没有考虑完全,比如select项目中可能有子查询还有可能有case表达式,这些我们都没考虑,这里只是先展示了一种思路。
    @Override
    public void visit(SelectExpressionItem selectExpressionItem) {
        if (Function.class.isInstance(selectExpressionItem.getExpression())) {
            Function function = (Function) selectExpressionItem.getExpression();
            function.accept(this);
        }
    }

    @Override
    public void visit(Function function) {
        if (function.getName().equalsIgnoreCase("max")){
            function.setName("min");
        }
    }

}

至此,max转min已经结束,我们再回过头实现FromItemVisitor接口:

public class MyJSqlVisitor implements StatementVisitor, SelectVisitor ,SelectItemVisitor, ExpressionVisitor,FromItemVisitor {
    
    @Override
    public void visit(Select select) {
        SelectBody selectBody = select.getSelectBody();
        if (selectBody != null) {
            selectBody.accept(this);
        }
    }

    @Override
    public void visit(PlainSelect plainSelect) {
        /** 处理select字段 */
        List<SelectItem> selectItems = plainSelect.getSelectItems();
        if (selectItems != null && selectItems.size() > 0) {
            selectItems.forEach(selectItem -> {
                selectItem.accept(this);
            });
        }

        /** 处理表名或子查询 */
        FromItem fromItem = plainSelect.getFromItem();
        if (fromItem!=null){
            fromItem.accept(this);
        }
    }

    // 这个方法我们并没有考虑完全,比如select项目中可能有子查询还有可能有case表达式,这些我们都没考虑,这里只是先展示了一种思路。
    @Override
    public void visit(SelectExpressionItem selectExpressionItem) {
        if (Function.class.isInstance(selectExpressionItem.getExpression())) {
            Function function = (Function) selectExpressionItem.getExpression();
            function.accept(this);
        }
    }

    // 实现将max函数转为min函数
    @Override
    public void visit(Function function) {
        if (function.getName().equalsIgnoreCase("max")){
            function.setName("min");
        }
    }

    //实现表名称的更换
    @Override
    public void visit(Table table) {
        if (table.getName().equalsIgnoreCase("table1")){
            table.setName("table2");
        }
    }

}

至此,我们的两个修改目标已经达成,运行main看下效果:

old sql:[{}]select max(age) from table1
new sql:[{}]SELECT min(age) FROM table2

Process finished with exit code 0

可以看到我们的目的实现了,不过这里请留意我们并没有考虑子查询等其它情况,这个demo只是展示一种修改思路,工作中具体的操作要考虑的比这细致的多。

使用建议:

1)一个个的添加接口,遇到什么类型的访问者,加什么类型的实现接口,防止一次性加太多忘记实现逻辑。

2)不要试图追踪各个sql对象的迭代处理情况,这样很容易把你绕进去,你只需关注当前所在的方法模块即可,其它的通过accpet交给其它对应的visitor去处理即可。

3)不要试图一次性实现所有的访问者接口,根据需要进行实现

4)sql语法树具有很强的层次性,当被访问者在进行处理时,要考虑到自己的子元素是不是也要进行迭代处理,如果需要的话,那么就调用对应子元素的accpect方法,并将相关访问者传递进去

5)如果没有使用容器技术,所有的访问者接口尽量放在一个类中实现,这样当有accept需要visitor对象的时候直接传this就行。(我一开始没有用容器管理bean,每个visitor接口我都单独创建一个实现类,最后因为使用不到,造成迭代访问时栈溢出错误)

4、核心原理介绍

这块只是展示sql迭代访问修改的原理,并不涉及将sql文本解析为对象类的原理。好了,进入正文。

要想理解sql迭代修改的原理,其实只要了解访问者模式多态这两个知识点就行。如果不了解的可以先去查看对应的知识点,然后再看下源码仔细体会下。下面我会简单介绍下,在前文我们也提过,要想修改sql,只需要实现对应的访问接口即可,然后将访问者传入被访问的sql对象中。

在JSqlParse中,将解析后的sql对象看做被访问者,我们自定义的visitor则看做访问者。该组件同时将各类被访问者和访问者都抽象出了接口,我们代码编辑时通过接口确定大体的执行流程,在具体的代码运行阶段,就会通过多态寻找对应的实现类。就拿demo中的statement来说,它是一个接口,但是运行的时候就会根据sql情况定位到具体的实现类,我们demo中对应的具体实现类就是select对象,此时进入该对象查看具体的accept方法:

可以看到被访问者调用的还是访问者的visit方法,也就是我们对应的重写方法。以此类推,剩下的各个层级处理也是通过重复这个过程,所以想理解这个处理过程,一定要理解访问者模式

;