Java SQL 解析器实践
SqlParseradmin 发布于:2023-07-29 15:34:54
阅读:loading
最近在刚好写了一个批量执行多个SQL脚本需求,脚本中的各个语句可以是各式各样的(可以是DDL,也可以是DML等),若是建立在比较简单约定的需求上直接使用JDBC的PreparedStatement对象中执行execute函数完活,它可以一次性执行多个“;”分割的脚本,执行完活拉到,不需要考虑使用分号分割的多个脚本执行的结果,但是这始终不是一个专业技术人员应该体现的水平。若是使用split拆分分号(或是正则分割分号或回车符)则显得非常鸡肋,会被诟病倒是小事儿,对脚本执行的结果不可控则显得问题更加严重。
所以前面一篇《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文件转换为各种结构化文本格式,并支持浮动内容表;
/**
* 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。
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);
}
特别说明
(a)上述脚本中分别罗列了创建表、创建索引、修改表、查询语句、合并语句、新增语句、修改语句、删除语句、提交语句、注释语句等多种语句场景,均被识别在有效范围内;
(b)上述脚本的执行后跳过了一些注释语句,无论是整行注释还是行尾注释;
(c)上述脚本将对应的脚本分门别类,我们可以按照对应的类型来使用相关的JDBC API加以执行,比如说:SELECT类的使用executeQuery来获取查询的元数据和查询的数据明细;Insert、Update、Delete类的使用executeUpdate来获取影响数据的具体行数;其它场景使用execute万能脚本执行;(补充一点若真有相关的测试场景的执行,需设置查询的超时时间、查询提取的最大数据条数等)
(d)若将一些脚本拆分好后,需要注意事务的一致性,具体需视对应的脚本;
去除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语句;
(1)相关源码工程点击此处下载:源码工程.txt;
(2)在JSqlParser中对于要解析的 SQL 脚本语法存在一定的校验,若语法不正确程序可能不会正常运行,所以需要保证脚本的语法正确无误;
(3)个人觉得Java SQL 格式化库“JSQLFormatter”值得拥有,所以下一篇将介绍它。
点赞