Java SQL 解析器实践

SqlParser
placeholder image
admin 发布于:2023-07-29 15:34:54
阅读:loading

最近在刚好写了一个批量执行多个SQL脚本需求,脚本中的各个语句可以是各式各样的(可以是DDL,也可以是DML等),若是建立在比较简单约定的需求上直接使用JDBC的PreparedStatement对象中执行execute函数完活,它可以一次性执行多个“;”分割的脚本,执行完活拉到,不需要考虑使用分号分割的多个脚本执行的结果,但是这始终不是一个专业技术人员应该体现的水平。若是使用split拆分分号(或是正则分割分号或回车符)则显得非常鸡肋,会被诟病倒是小事儿,对脚本执行的结果不可控则显得问题更加严重。

1.关于JSqlParser

所以前面一篇《Java获取SQL语句中的表名》的文章中,使用到的组件“sql-table-name-parser”介绍中也提及到了本文的主角《JSqlParser》,接着实践它可以拆分多个SQL脚本的特性来介绍一下我对它的了解,认识它也就才一两天,在它身上花的时间还不到半天,所以可能它非常的强大,我只是了解它的一小块,大家不好仅限于我所提及到的知识,可以自行去了解掌握。

JSQLParser是一个基于 JavaCC 构建的 SQL 语句解析器,它将 SQL 转换为可遍历的 Java 类层次结构,同时是一个用于解析和操作SQL语句的Java库。提供了一组功能强大的API,可以帮助你解析SQL语句的各个部分,如SELECT子句、FROM子句、WHERE子句等,并且可以对解析后的SQL语句进行修改、重构和生成。

JSqlParser的一些主要功能和特点

(1)SQL语句解析:JSqlParser可以将输入的SQL语句解析成一个抽象语法树(AST),并提供了访问AST节点的API。你可以使用这些API来获取SQL语句的各个部分,如表名、列名、条件表达式等。

(2)SQL语句修改和重构:通过JSqlParser,你可以对解析后的SQL语句进行修改和重构。你可以添加、删除或修改AST节点,以实现对SQL语句的定制化操作。

(3)SQL语句生成:JSqlParser还支持根据AST节点生成SQL语句。你可以通过创建和组装AST节点来构建一个SQL查询,然后使用JSqlParser提供的API将其转换为相应的SQL语句。

(4)多种SQL语句支持:JSqlParser支持解析和操作多种类型的SQL语句,包括SELECT查询、INSERT语句、UPDATE语句、DELETE语句等。它还支持各种SQL语句的子句,如JOIN子句、WHERE子句、ORDER BY子句等。可以使用JSqlParser的不同类来处理特定类型的SQL语句。例如,Select类用于处理SELECT查询,Insert类用于处理INSERT语句,Update类用于处理UPDATE语句等。

(5)使用JSqlParser,你可以在Java应用程序中轻松地解析、修改和生成SQL语句。无论是构建自定义的SQL查询构造器,还是实现SQL语句的自动化处理,JSqlParser都是一个强大而灵活的工具。此外这个组件的官方另有其它几款开源库介绍,详细如下:

(a)Java SQL 格式化库:提供的独立于平台的 SQL 格式化程序、美化程序和打印;

(b)H2迁移工具:一个Java 应用程序,用于将H2 数据库文件从旧版本迁移到新版本。它将使用旧的 H2 驱动程序将现有数据库导出到 SQL,然后使用新的驱动程序创建新数据库。它还支持损坏数据库的恢复以及从 SQL 脚本直接创建;

(c)MJdbcUtils:一个Java库,用于处理查询或 DML 或 DDL 语句中的命名参数(例如:parameter)。它要么 用位置参数替换任何命名参数,要么用参数的值重写命名参数,并在命名参数和提供的值之间提供方便的映射。此外,它还为参数对话框和参数批量更新提供帮助程序;

(d)JDBCParquetWriter:一个Java库,用于从JDBC表或结果集写入Apache Parquet文件。它使用Apache Hadoop和 Parquet 将 JDBC 行转换为基于列的格式。Parquet文件可以导入到基于列的分析数据库中;

(e)XMLDoclet:一个用于从Java源文档编写XML文件的Doclet,它附带XSL样式表,可将XML文件转换为各种结构化文本格式,并支持浮动内容表;

2.SQL语句生成

/**
 * SQL语句生成
 * 【输出结果】SELECT *, 'chendd' AS name, website FROM users t1
 */
@Test
public void createSql() {
    Select select = new Select();
    PlainSelect plainSelect = new PlainSelect();
    Table table = new Table("users").withAlias(new Alias("t1", false));

    List<SelectItem> list = new ArrayList<>();
    list.add(new AllColumns());
    list.add(new SelectExpressionItem(new StringValue("chendd")).withAlias(new Alias("name" , true)));
    list.add(new SelectExpressionItem(new Column("website")));
    plainSelect.setSelectItems(list);
    plainSelect.setFromItem(table);
    select.setSelectBody(plainSelect);
    System.out.println(select.toString());
}

特别说明

这些构造表的API需要更多的时间去学习,做一些深度的掌握,本示例比较简单,在实际应用中显得比较鸡肋,若实际有需要此类需求,还是MyBatis中的SQL类构造起来比较简单,直接面向SQL语句的编写层面,对于查询字段的定义和表的关联及查询条件等方面更加简介易用,所以个人不推荐使用这套API。

3.多种SQL语句支持

SQL脚本节选

-- ORACLE ALTER TABLE ADD COLUMN
ALTER TABLE risk.collateral
    ADD id_status VARCHAR (1) NULL
;

-- 查询SQL
SELECT DISTINCT
            a.id_collateral , 'A' AA , ' ; ' BB
        FROM cfe.collateral a
            LEFT JOIN cfe.collateral_ref b
                ON a.id_collateral = b.id_collateral
        WHERE b.id_collateral_ref IS NULL;

UPDATE cfe.calendar
SET     year_offset = ?                    /* year offset */
        , settlement_shift = To_Char( ? )  /* settlement shift */
        , friday_is_holiday = ?            /* friday is a holiday */
        , saturday_is_holiday = ?          /* saturday is a holiday */
        , sunday_is_holiday = ?            /* sunday is a holiday */
WHERE id_calendar = ?;

COMMIT;

--增加表注释
comment on table CHENDD is '表注释';

--增加字段注释
comment on column CHENDD.id is '主键ID';

DROP TABLE `www.chendd.cn`;

TRUNCATE TABLE `www.chendd.cn`;

DELETE FROM `www.chendd.cn`;

执行代码

/**
 * 获取脚本文件的执行类型和语句
 */
@Test
public void parseSql() throws Exception {
    Path path = new File(getClass().getResource("/script.sql").getFile()).toPath();
    String sql = new String(Files.readAllBytes(path));
    Statements statements = CCJSqlParserUtil.parseStatements(sql);
    List<Statement> list = statements.getStatements();
    for (Statement statement : list) {
        System.out.println(statement.getClass().getSimpleName() + ":" + statement.toString());
    }
    System.out.println(list);
}

image.png

特别说明

(a)上述脚本中分别罗列了创建表、创建索引、修改表、查询语句、合并语句、新增语句、修改语句、删除语句、提交语句、注释语句等多种语句场景,均被识别在有效范围内;

(b)上述脚本的执行后跳过了一些注释语句,无论是整行注释还是行尾注释;

(c)上述脚本将对应的脚本分门别类,我们可以按照对应的类型来使用相关的JDBC API加以执行,比如说:SELECT类的使用executeQuery来获取查询的元数据和查询的数据明细;Insert、Update、Delete类的使用executeUpdate来获取影响数据的具体行数;其它场景使用execute万能脚本执行;(补充一点若真有相关的测试场景的执行,需设置查询的超时时间、查询提取的最大数据条数等)

(d)若将一些脚本拆分好后,需要注意事务的一致性,具体需视对应的脚本;

4.去除SQL排序

去除SQL语句中的Order by语句也是经常会使用到的,并且是有必要的,如果你未曾注意到这一点也很正常,因为使用到的框架帮我们做好了,比如MyBatis Plus和Spring Data JPA等框架内部均有处理,主要体现在我们编写的查询分页SQL语句在输出时,若语句包含有Order by语句时,在输出的 count (*) 语句中去除了Order by部分的实现,使得查询时更加高效,本身count语句更排序也没啥关系,而往往查询分页的程序为了便捷只需要关注查询明细数据的SQL编写。

在JSqlParser中也有专门的实现方式,参考代码如下:

/**
 * 去除Order语句和Group
 * 输出【SELECT id, name, sum(age) age FROM users WHERE age > 18 GROUP BY id, name】
 */
@Test
public void removeOrder() throws Exception {
    String sql = "SELECT id, name , sum(age) age FROM users WHERE age > 18 group by id , name ORDER BY name ASC , age DESC";
    Select select = (Select) CCJSqlParserUtil.parse(sql);
    PlainSelect selectBody = (PlainSelect) select.getSelectBody();
    List<OrderByElement> orderElements = selectBody.getOrderByElements();
    System.out.println(orderElements);
    // 移除排序部分
    selectBody.setOrderByElements(null);
    //  移除分组部分
    /*plainSelect.setGroupByElement(null);*/
    System.out.println(select.toString());
}

特别说明

(a)去除Order by是有必要的,至于Group语句需要具体分析必要性,本代码是验证可以去除Group语句;

5.相关下载

(1)相关源码工程点击此处下载:源码工程.txt

(2)JSqlParser中对于要解析的 SQL 脚本语法存在一定的校验,若语法不正确程序可能不会正常运行,所以需要保证脚本的语法正确无误

(3)个人觉得Java SQL 格式化库“JSQLFormatter”值得拥有,所以下一篇将介绍它。


 点赞


 发表评论

当前回复:作者

 评论列表


留言区