草庐IT

Sql解析转换之JSqlParse完整介绍

Interest1_wyt 2024-04-28 原文

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方法,也就是我们对应的重写方法。以此类推,剩下的各个层级处理也是通过重复这个过程,所以想理解这个处理过程,一定要理解访问者模式

有关Sql解析转换之JSqlParse完整介绍的更多相关文章

  1. Ruby 解析字符串 - 2

    我有一个字符串input="maybe(thisis|thatwas)some((nice|ugly)(day|night)|(strange(weather|time)))"Ruby中解析该字符串的最佳方法是什么?我的意思是脚本应该能够像这样构建句子:maybethisissomeuglynightmaybethatwassomenicenightmaybethiswassomestrangetime等等,你明白了......我应该一个字符一个字符地读取字符串并构建一个带有堆栈的状态机来存储括号值以供以后计算,还是有更好的方法?也许为此目的准备了一个开箱即用的库?

  2. ruby-on-rails - 在 Rails 中将文件大小字符串转换为等效千字节 - 2

    我的目标是转换表单输入,例如“100兆字节”或“1GB”,并将其转换为我可以存储在数据库中的文件大小(以千字节为单位)。目前,我有这个:defquota_convert@regex=/([0-9]+)(.*)s/@sizes=%w{kilobytemegabytegigabyte}m=self.quota.match(@regex)if@sizes.include?m[2]eval("self.quota=#{m[1]}.#{m[2]}")endend这有效,但前提是输入是倍数(“gigabytes”,而不是“gigabyte”)并且由于使用了eval看起来疯狂不安全。所以,功能正常,

  3. ruby - 使用 ruby​​ 将 HTML 转换为纯文本并维护结构/格式 - 2

    我想将html转换为纯文本。不过,我不想只删除标签,我想智能地保留尽可能多的格式。为插入换行符标签,检测段落并格式化它们等。输入非常简单,通常是格式良好的html(不是整个文档,只是一堆内容,通常没有anchor或图像)。我可以将几个正则表达式放在一起,让我达到80%,但我认为可能有一些现有的解决方案更智能。 最佳答案 首先,不要尝试为此使用正则表达式。很有可能你会想出一个脆弱/脆弱的解决方案,它会随着HTML的变化而崩溃,或者很难管理和维护。您可以使用Nokogiri快速解析HTML并提取文本:require'nokogiri'h

  4. ruby - 解析 RDFa、微数据等的最佳方式是什么,使用统一的模式/词汇(例如 schema.org)存储和显示信息 - 2

    我主要使用Ruby来执行此操作,但到目前为止我的攻击计划如下:使用gemsrdf、rdf-rdfa和rdf-microdata或mida来解析给定任何URI的数据。我认为最好映射到像schema.org这样的统一模式,例如使用这个yaml文件,它试图描述数据词汇表和opengraph到schema.org之间的转换:#SchemaXtoschema.orgconversion#data-vocabularyDV:name:namestreet-address:streetAddressregion:addressRegionlocality:addressLocalityphoto:i

  5. ruby - 将数组的内容转换为 int - 2

    我需要读入一个包含数字列表的文件。此代码读取文件并将其放入二维数组中。现在我需要获取数组中所有数字的平均值,但我需要将数组的内容更改为int。有什么想法可以将to_i方法放在哪里吗?ClassTerraindefinitializefile_name@input=IO.readlines(file_name)#readinfile@size=@input[0].to_i@land=[@size]x=1whilex 最佳答案 只需将数组映射为整数:@land边注如果你想得到一条线的平均值,你可以这样做:values=@input[x]

  6. ruby - 将散列转换为嵌套散列 - 2

    这道题是thisquestion的逆题.给定一个散列,每个键都有一个数组,例如{[:a,:b,:c]=>1,[:a,:b,:d]=>2,[:a,:e]=>3,[:f]=>4,}将其转换为嵌套哈希的最佳方法是什么{:a=>{:b=>{:c=>1,:d=>2},:e=>3,},:f=>4,} 最佳答案 这是一个迭代的解决方案,递归的解决方案留给读者作为练习:defconvert(h={})ret={}h.eachdo|k,v|node=retk[0..-2].each{|x|node[x]||={};node=node[x]}node[

  7. ruby - 用逗号、双引号和编码解析 csv - 2

    我正在使用ruby​​1.9解析以下带有MacRoman字符的csv文件#encoding:ISO-8859-1#csv_parse.csvName,main-dialogue"Marceu","Giveittohimóhe,hiswife."我做了以下解析。require'csv'input_string=File.read("../csv_parse.rb").force_encoding("ISO-8859-1").encode("UTF-8")#=>"Name,main-dialogue\r\n\"Marceu\",\"Giveittohim\x97he,hiswife.\"\

  8. ruby-on-rails - Ruby url 到 html 链接转换 - 2

    我正在使用Rails构建一个简单的聊天应用程序。当用户输入url时,我希望将其输出为html链接(即“url”)。我想知道在Ruby中是否有任何库或众所周知的方法可以做到这一点。如果没有,我有一些不错的正则表达式示例代码可以使用... 最佳答案 查看auto_linkRails提供的辅助方法。这会将所有URL和电子邮件地址变成可点击的链接(htmlanchor标记)。这是文档中的代码示例。auto_link("Gotohttp://www.rubyonrails.organdsayhellotodavid@loudthinking.

  9. ruby-on-rails - 我更新了 ruby​​ gems,现在到处都收到解析树错误和弃用警告! - 2

    简而言之错误:NOTE:Gem::SourceIndex#add_specisdeprecated,useSpecification.add_spec.Itwillberemovedonorafter2011-11-01.Gem::SourceIndex#add_speccalledfrom/opt/local/lib/ruby/site_ruby/1.8/rubygems/source_index.rb:91./opt/local/lib/ruby/gems/1.8/gems/rails-2.3.8/lib/rails/gem_dependency.rb:275:in`==':und

  10. ruby-on-rails - 使用 ruby​​ 将多个实例变量转换为散列的更好方法? - 2

    我收到格式为的回复#我需要将其转换为哈希值(针对活跃商家)。目前我正在遍历变量并执行此操作:response.instance_variables.eachdo|r|my_hash.merge!(r.to_s.delete("@").intern=>response.instance_eval(r.to_s.delete("@")))end这有效,它将生成{:first="charlie",:last=>"kelly"},但它似乎有点hacky和不稳定。有更好的方法吗?编辑:我刚刚意识到我可以使用instance_variable_get作为该等式的第二部分,但这仍然是主要问题。

随机推荐