《通俗易懂 · JSqlParser 解析和构造SQL》
📢 大家好,我是 【战神刘玉栋】,有10多年的研发经验,致力于前后端技术栈的知识沉淀和传播。 💗
🌻 希望大家多多支持,后续会继续提升文章质量,绝不滥竽充数,欢迎多多交流。👍
文章目录
- 写在前面的话
- 技术简介
- 基础间接
- 特色说明
- 核心功能
- 竞品分析
- 技术扩展
- 基础入门
- 主要模块
- 解析SQL能力
- 构建SQL能力
- 企业实战
- 运用场景
- 企业实战 - SQL 解析
- 企业实战 - SQL 生成
- 总结陈词
写在前面的话
博主公司在近期新产品的技术选型当中,涉及到SQL解析与生成等场景,这部分选择了JSqlParser
,这里进行简单的介绍说明,大家可以按需选择。
参考:JSqlParser 官网
技术简介
基础间接
JSqlParser 是一个功能强大的 Java 库,专为解析、修改和生成 SQL 语句而设计。它不仅能够处理复杂的 SQL 语法,还提供了丰富的 API,使得开发者可以轻松地对 SQL 语句进行各种操作。JSqlParser 的主要目标是为开发人员提供一个可靠的工具,以便在应用程序中高效地管理和优化 SQL 语句。
JSqlParser 的设计理念是简单易用,同时具备高度的灵活性和扩展性。无论是初学者还是经验丰富的开发人员,都可以通过 JSqlParser 快速上手并解决实际问题。该库支持多种 SQL 方言,包括 MySQL、PostgreSQL、Oracle 和 SQL Server 等,这使得它在多数据库环境中具有广泛的应用前景。
特色说明
1、功能丰富
JSqlParser 提供了全面的 SQL 解析功能,可以解析几乎所有的 SQL 语句类型,包括但不限于 SELECT、INSERT、UPDATE、DELETE、CREATE TABLE 和 ALTER TABLE 等。此外,它还支持复杂的子查询、联合查询和嵌套查询等高级语法。通过 JSqlParser,开发人员可以轻松地将 SQL 语句转换为抽象语法树(AST),从而方便地进行进一步的操作和分析。
2、易于使用与扩展
JSqlParser 的 API 设计简洁明了,使得开发人员可以快速上手并集成到现有的项目中,解析或生成一个简单的 SQL 语句只需要几行代码。
3、高度可扩展
JSqlParser 不仅提供了丰富的内置功能,还允许开发人员根据需要进行扩展。例如,可以通过实现自定义的 Visitor 接口来遍历和修改抽象语法树。这种灵活性使得 JSqlParser 可以适应各种复杂的应用场景,如 SQL 语句的动态生成、查询优化和安全性检查等。
4、支持多种 SQL 方言
JSqlParser 支持多种主流的 SQL 方言,包括 MySQL、PostgreSQL、Oracle 和 SQL Server 等。这意味着开发人员可以在不同的数据库环境中使用同一个库,而无需担心兼容性问题。这种跨平台的支持使得 JSqlParser 成为了一个多数据库应用的理想选择。
核心功能
前面已经写了太多文字了,核心功能不展开了。
无非就是:SQL解析、SQL调整、SQL生成。
再简化一下:SQL解析和生成!
竞品分析
JSqlParser 同类型的工具,用于解析 SQL 语句,主要有以下几种:
- **SQLparser: **一个轻量级的 Java 库,专注于 SQL 解析,支持多种 SQL 方言,功能相对 JSqlParser 较为精简。
- **Apache Calcite: **一个用于 SQL 查询优化的框架,也包含 SQL 解析器, 更侧重于查询优化和执行,解析功能作为其一部分。
- **ANTLR: ** 一个强大的语法分析工具,可以用于构建自定义的 SQL 解析器,灵活性高,但需要用户定义语法规则。
- **SQL-Java: **一个用于解析和操作 SQL 语句的 Java 库,功能与 JSqlParser 类似,但可能在特定方面有不同侧重。
温馨提示:个人认为,这种工具类的,不用花费大量时间去纠结技术选型,能满足开发需求,用的顺手即可。
技术扩展
基础入门
Tips:截止编写日期,最新版本是5.0,需要使用JDK11以上版本。
Tips:针对后端框架没有限制,可以当作 Hutools 那样的工具类使用。
Step1、引入Maven依赖
<dependency>
<groupId>com.github.jsqlparser</groupId>
<artifactId>jsqlparser</artifactId>
<version>5.0</version>
</dependency>
Step2、编写SQL解析的测试方法
@Test
public void parseSqlTemp() throws JSQLParserException {
String sql = "SELECT id,name FROM staff_member WHERE nickname= '刘'";
Statement statement = CCJSqlParserUtil.parse(sql);
if (statement instanceof Select selectStatement) {
PlainSelect plainSelect = selectStatement.getPlainSelect();
log.info("==> JsqlParser SQL: {}", selectStatement);
log.info("==> FromItem: {}", plainSelect.getFromItem());
log.info("==> SelectItem: {}", plainSelect.getSelectItems());
log.info("==> Where: {}", plainSelect.getWhere());
log.info("==> Joins: {}", plainSelect.getJoins());
}
}
// 输出信息:
==> JsqlParser SQL: SELECT id, name FROM staff_member WHERE nickname = '刘'
==> FromItem: staff_member
==> SelectItem: [id, name]
==> Where: nickname = '刘'
Step3、编写SQL生成的测试方法
@Test
public void testSql1() {
Table t1 = new Table("tab1").withAlias(new Alias("t1").withUseAs(true)); // 表1
Table t2 = new Table("tab2").withAlias(new Alias("t2", false)); // 表2
PlainSelect plainSelect = new PlainSelect().addSelectItems(new AllColumns())
.withFromItem(t1); // SELECT * FROM tab1 AS t1
Join join = new Join(); // 创建Join对象
join.withRightItem(t2); // 添加Join的表 JOIN t2 =>JOIN tab2 t2
EqualsTo equalsTo = new EqualsTo(); // 添加 = 条件表达式 t1.user_id = t2.user_id
equalsTo.setLeftExpression(new Column(t1, "user_id "));
equalsTo.setRightExpression(new Column(t2, "user_id "));
join.withOnExpression(equalsTo);// 添加ON
plainSelect.addJoins(join);
System.err.println(plainSelect);
// SELECT * FROM tab1 AS t1 JOIN tab2 t2 ON t1.user_id = t2.user_id
}
主要模块
1. Expression
expression 包含了 SQL 中的各种表达式类,这些类用于构建和表示 SQL 语句中的条件和表达式。常见的表达式类包括:
EqualsTo:表示等于操作符(=)。
InExpression:表示 IN 操作符,用于检查某个值是否在一组值中。
BinaryExpression:表示二元操作符(如加法、减法等)。
Function:表示 SQL 函数调用。
Column:表示 SQL 中的列。
这些表达式类使得用户可以方便地构建复杂的 SQL 查询条件。
2. Schema
schema 模块主要用于表示数据库的结构,包括表、列、约束等。常见的类包括:
Table:表示数据库中的表。
Column:表示表中的列,通常包含列名、数据类型等信息。
Index:表示表上的索引。
ForeignKey:表示表之间的外键关系。
这些类帮助用户理解和操作数据库的结构,便于进行元数据的管理和查询。
3. Parser
parser 模块包含了 SQL 解析的相关类,主要负责将 SQL 字符串解析为 Java 对象。常见的类包括:
CCJSqlParserUtil:提供了静态方法来解析 SQL 语句,返回相应的语法树。
SQLParser:核心解析器,负责将 SQL 语句分解为不同的部分。
ExpressionParser:用于解析 SQL 中的表达式部分。
通过这些类,用户可以轻松地将 SQL 语句转换为可操作的 Java 对象。
4. Statement
statement 模块封装了各种数据库操作的对象,表示不同类型的 SQL 语句。常见的类包括:
Select:表示 SELECT 查询语句。
Insert:表示 INSERT 操作。
Update:表示 UPDATE 操作。
Delete:表示 DELETE 操作。
CreateTable:表示创建表的语句。
这些类使得用户可以方便地构建、修改和执行 SQL 语句。
解析SQL能力
JSqlParser可以解析SQL为Java对象,以便于获取SQL中的相关信息或可进行修改。
一般通过 CCJSqlParserUtil 和 CCJSqlParserManager 解析SQL。
CCJSqlParserUtil 适合于简单的情况,CCJSqlParserManager 则拥有更强大的功能。
【示例代码】
这边来一段示例代码,其实自己准备一个SQL,尝试一下很容易理解。
基本步骤:使用CCJSqlParserUtil解析sql,获得Statement,如果是查询就转换PlainSelect类型,然后调其相关方法获取更详细的内容。
@Test
public void testSelectAll() throws JSQLParserException {
// 准备一段基础SQL
String sql = """
SELECT t.pres_no,
t.patient_id,
t.exec_dept_code,
t1.patient_id,
t1.patient_name,
t1.patient_sex,
(SELECT a.DEPT_NAME
FROM ZOEDICT.DIC_DEPT_DICT a
WHERE a.DEPT_CODE = t.exec_dept_code) AS "EXEC_DEPT_NAME"
FROM ZOEPRES.PRES_OUTP_PRES_MASTER t
JOIN zoepatient.pat_basic_info t1 ON t.patient_id = t1.patient_id
WHERE t.patient_id = '5000022933'
AND t.event_no = 'D3389411'
""";
// 利用 CCJSqlParserUtil 解析SQL
Statement statementTemp = CCJSqlParserUtil.parse(sql);
if (statementTemp instanceof Select selectStatement) {
PlainSelect plainSelect = selectStatement.getPlainSelect();
log.info("===> JsqlParser SQL: {}", selectStatement);
log.info("===> FromItem: {}", plainSelect.getFromItem());
log.info("===> SelectItem: {}", plainSelect.getSelectItems());
log.info("===> Where: {}", plainSelect.getWhere());
log.info("===> Join: {}", plainSelect.getJoins());
}
// 创建 TablesNamesFinder 实例,提取表名
// 这里会连JOIN和子查询的表一起输出,总共三张
TablesNamesFinder tablesNamesFinder = new TablesNamesFinder();
List<String> tableList = tablesNamesFinder.getTableList(statementTemp);
System.out.println(tableList);
}
【PlainSelect 方法列举】
获取和设置表(From子句):
FromItem getFromItem(): 获取FROM子句中的表或子查询。
void setFromItem(FromItem fromItem): 设置FROM子句中的表或子查询。
获取和设置选择项(SelectItems):
List<SelectItem> getSelectItems(): 获取SELECT子句中的选择项列表。
void setSelectItems(List<SelectItem> selectItems): 设置SELECT子句中的选择项列表。
获取和设置WHERE子句:
Expression getWhere(): 获取WHERE子句的条件表达式。
void setWhere(Expression where): 设置WHERE子句的条件表达式。
获取和设置GROUP BY子句:
List<Expression> getGroupByColumnReferences(): 获取GROUP BY子句中的列引用列表。
void setGroupByColumnReferences(List<Expression> groupByColumnReferences): 设置GROUP BY子句中的列引用列表。
获取和设置ORDER BY子句:
List<OrderByElement> getOrderByElements(): 获取ORDER BY子句中的排序元素列表。
void setOrderByElements(List<OrderByElement> orderByElements): 设置ORDER BY子句中的排序元素列表。
获取和设置LIMIT子句:
Limit getLimit(): 获取LIMIT子句。
void setLimit(Limit limit): 设置LIMIT子句。
获取和设置DISTINCT关键字:
boolean isDistinct(): 检查SELECT语句是否使用了DISTINCT关键字。
void setDistinct(boolean distinct): 设置SELECT语句是否使用DISTINCT关键字。
获取和设置INTO子句(用于SELECT INTO语句):
SubSelect getIntoTables(): 获取INTO子句中的表。
void setIntoTables(SubSelect intoTables): 设置INTO子句中的表。
获取和设置HAVING子句:
Expression getHaving(): 获取HAVING子句的条件表达式。
void setHaving(Expression having): 设置HAVING子句的条件表达式。
获取和设置别名:
String getAlias(): 获取SELECT语句的别名。
void setAlias(String alias): 设置SELECT语句的别名。
获取和设置子查询(SubSelect):
SubSelect getSubSelect(): 获取子查询。
void setSubSelect(SubSelect subSelect): 设置子查询。
获取和设置联合查询(Union):
List<PlainSelect> getUnion(): 获取联合查询的SELECT语句列表。
void setUnion(List<PlainSelect> union): 设置联合查询的SELECT语句列表。
构建SQL能力
构建SQL其实相当于解析SQL的逆过程,是构造组装一个PlainSelect的过程,进而得到SQL语句。
可以从无到有构造SQL,也可以基于原有SQL,解析出PlainSelect对象后,再进行修改。
操作方法很简单,直接看下方示例,企业实战中,通常会搭配自定义语义解析模板使用。
【示例代码】
@Test
public void buildSqlTemp() throws JSQLParserException {
// 定义表
Table table = new Table().withName("zoepres.pres_outp_pres_master")
.withAlias(new Alias("t", false));
// 定义查询列
Column columnA = new Column().withColumnName("patient_id");
Column columnB = new Column().withColumnName("pres_no");
Column columnC = new Column().withColumnName("event_no");
// 定义查询列的条件
Expression whereExpression = new EqualsTo().withLeftExpression(columnA)
.withRightExpression(new StringValue("5000022928"));
// 定义查询
PlainSelect select = new PlainSelect().addSelectItem(new LongValue(123))
.withFromItem(table)
.withWhere(whereExpression);
// 增加函数表达式
select.addSelectItem(columnB, new Alias("presNo"));
// 增加子查询
Expression expr2 = CCJSqlParserUtil.parseExpression("(select count(1) from dual)");
select.addSelectItem(expr2, new Alias("id124"));
// 增加函数处理逻辑
Function function = new Function();
function.setName("REPLACE");
List<Expression> parameters = new ArrayList<>();
parameters.add(columnC);
parameters.add(new StringValue("a"));
parameters.add(new StringValue("b"));
function.setParameters(new ExpressionList(parameters));
select.addSelectItem(function);
System.out.println(select);
}
//输出信息如下:
SELECT 123,
pres_no AS presNo,
(SELECT count(1) FROM dual) AS id124,
REPLACE(event_no, 'a', 'b')
FROM zoepres.pres_outp_pres_master t
WHERE patient_id = '5000022928'
企业实战
运用场景
Java SQL 解析器(如 JSQLParser)在多个场景中发挥着重要作用,以下是一些具体的应用场景:
1、构建数据库管理工具:在开发数据库客户端或管理界面时,Java SQL 解析器可以解析用户输入的 SQL 查询。这使得工具能够执行相应的操作,例如执行查询、更新数据库结构或管理数据,从而提升用户体验和操作效率。
2、实现自定义的 SQL 分析工具:当需要对大量 SQL 查询进行深入分析时,Java SQL 解析器可以帮助解析这些查询,以识别查询模式、性能瓶颈等。通过编写自定义分析逻辑,开发者可以获得更深入的洞察,优化数据库性能。
3、定制 SQL 解析和执行逻辑:在某些情况下,标准的数据库接口可能无法满足特定需求。此时,Java SQL 解析器可以解析 SQL 查询,并允许开发者编写自定义的执行逻辑,以实现更复杂的功能或满足特定的业务需求。
4、实现查询优化器:如果希望深入了解查询优化器的工作原理,Java SQL 解析器可以解析 SQL 查询,并基于解析结果实现自己的查询优化器。这为开发者提供了一个实验和学习的机会,以优化查询性能。
5、实现 SQL 注入检测工具:SQL 注入是常见的安全漏洞之一。为了防止 SQL 注入攻击,开发者可使用Java 的SQL解析器解析用户输入的 SQL 查询,并检测其中是否包含潜在的注入漏洞。这种检测机制有助于增强应用程序的安全性,保护数据库免受攻击。
通过这些应用场景,可以看出 Java SQL 解析器在数据库管理、性能优化、安全检测等方面的重要性,帮助开发者更高效地处理 SQL 查询和相关操作。
企业实战 - SQL 解析
【场景说明】
博主所在公司开发了数据中心产品线,涉及较多与数据打交道的数据中心工具和产品,诸如票据设计器、报表系统、床位导航等,这些产品都涉及可视化配置界面,需要通过工程人员编写的SQL语句,作为页面的数据来源,同时需要具备从SQL中解析出各项元素,进行后续操作。例如,提取出SELECT后面的元素,生成返回值表格列,允许设置表名、宽度、类型等内容。
【场景使用】
针对上述场景,使用JSQLParser
来实现,就相当简单了,很轻易的将SQL语法进行剖析,分解出各个组成部分,一览无余,按需使用。
只能说使用JSQLParser
来解析SQL,真是庖丁解牛、游刃有余。
企业实战 - SQL 生成
【场景说明】
无独有偶,博主所在公司继续开发低代码平台,部分产品线决定不通过写SQL完成配置,而是完全采用通俗易懂的拖拉拽和可视化表单编辑等方式,生成页面逻辑。为此,我们需要将前端元素转换为一个可执行的SQL语法。
【场景使用】
针对上述场景,使用JSQLParser
来实现,也是相当简单了。
当然,我们没有直接解析前端传参,然后进行JSQLParser
处理,而是在中间增加了一层JSON模板,前端根据该模板约定组装入参,后端增加将JSON模板使用JSQLParser
解析成相应SQL。通常这种方式,使查询语义可以在各个产品线更好的共享,增强了扩展性。
总结陈词
本篇文章大体介绍了JSqlParser
实现SQL的解析和生成,由于篇幅所限,仅展示简单示例分析。
💗 后续也会逐步分享企业实际开发中的实战经验,有需要交流的可以联系博主。