使用 JSqlParser 解析 SQL 语句

转自:Java 使用 JSqlParser 解析 SQL 语句总结

概述

JSqlParser 是一个开源的 Java SQL 解析库,用于分析和操作 SQL 语句的结构。该库支持多种数据库方言,提供丰富的 API 接口,能够实现 SQL 语句的解析、修改和生成功能。MyBatis Plus 中就用到了 JSqlParser 来实现拦截器。

快速使用

环境准备

将 JSqlParser 直接添加到项目中:

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

解析 SQL 语句

接下来使用 JSqlParser 去解析语句,注意解析 SQL 语句时,应确保语句结构正确,最好是在数据库中可直接执行的语句:

String sql = "SELECT id, name, nickname, age, job, department FROM staff_member WHERE nickname = '刘'";

// Parse SQL
Statement statement = CCJSqlParserUtil.parse(sql);
Select selectStatement = (Select) statement;

log.info("==> JSqlParser SQL: {}", selectStatement.toString());

正常情况下,将得到一个包含各种属性的 Statement 对象,这表明 SQL 语句已成功解析,并被映射到对象的各个属性中。

img

Statement 接口

熟悉 JDBC 的开发者都知道 Statement 表示语句的概念。在 JSqlParser 中,Statement 被设计为面向对象的接口。设计为接口的原因在于 JSqlParser 需要区分不同类型的 SQL 语句,如 Select、Insert、Delete 或 Create 等,因此对每种语句类型都进行了封装,并继承自 Statement 接口。

img

因此,不同类型的 SQL 语句都有对应的适配对象。例如 Select 语句对应net.sf.jsqlparser.statement.select.Select对象,Insert 语句也有相应的对象。通过将 Statement 强制转换为对应的具体类型,可以获取或修改其中的属性,这也是 SQL 解析的主要目的。

当 JSqlParser 成功解析 SQL 语句后,statement 对象已经确定了其具体类型:

String sql = "SELECT id, name, nickname, age, job, department FROM staff_member WHERE nickname = '刘'";

// Parse SQL
Statement statement = CCJSqlParserUtil.parse(sql);

if (statement instanceof Select) {
    Select selectStatement = (Select) statement;
    log.info("==> JSqlParser SQL: {}", selectStatement.toString());
}

if (statement instanceof Insert) {
    Insert insertStatement = (Insert) statement;
    log.info("==> JSqlParser SQL: {}", insertStatement.toString());
}

if (statement instanceof Update) {
    Update updateStatement = (Update) statement;
    log.info("==> JSqlParser SQL: {}", updateStatement.toString());
}

if (statement instanceof Delete) {
    Delete deleteStatement = (Delete) statement;
    log.info("==> JSqlParser SQL: {}", statement.toString());
}

分析语句

查询语句

当 statement 成功解析 SQL 语句后,可通过 PlainSelect 获取 SQL 语句中的各个元素:

String sql = "SELECT id, name, nickname, age, job, department FROM staff_member WHERE nickname = '刘'";

// Parse SQL
Statement statement = CCJSqlParserUtil.parse(sql);
if (statement instanceof Select) {
    Select selectStatement = (Select) statement;
    log.info("==> JSqlParser SQL: {}", selectStatement.toString());
    
    PlainSelect plainSelect = selectStatement.getPlainSelect();
    log.info("==> FromItem: {}", plainSelect.getFromItem());
    log.info("==> SelectItem: {}", plainSelect.getSelectItems());
    log.info("==> Where: {}", plainSelect.getWhere());
}

运行结果:

==> JSqlParser SQL: SELECT id, name, nickname, age, job, department FROM staff_member WHERE nickname = '刘'
==> FromItem: staff_member
==> SelectItem: [id, name, nickname, age, job, department]
==> Where: nickname = '刘'

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 语句列表。

新增语句

新增语句的处理方式与查询语句类似,但由于 Insert 语句结构相对简单,JSqlParser 没有设计类似 PlainSelect extends Select 的专门类,而是直接通过 Insert 对象获取和操作 Insert 语句中的内容:

String sql = "INSERT INTO employees (employee_id, employee_name, department) VALUES (1, 'John Doe', 'Human Resources')";

// Parse SQL
Statement statement = CCJSqlParserUtil.parse(sql);
if (statement instanceof Insert) {
    Insert insertStatement = (Insert) statement;
    log.info("==> JSqlParser SQL: {}", insertStatement.toString());
    log.info("==> Table: {}", insertStatement.getTable());
    log.info("==> Columns: {}", insertStatement.getColumns());
    log.info("==> ItemsList: {}", insertStatement.getValues());
}

运行结果:

==> JSqlParser SQL: INSERT INTO employees (employee_id, employee_name, department) VALUES (1, 'John Doe', 'Human Resources')
==> Table: employees
==> Columns: employee_id, employee_name, department
==> ItemsList: VALUES (1, 'John Doe', 'Human Resources')

Insert 常用方法

  • Table getTable(): 获取插入语句中的目标表。
  • List<Column> getColumns(): 获取插入语句中要插入的列的列表。
  • ItemsList getValues(): 获取插入语句中的值列表,可以是单个值或者子查询。
  • String getPrefix(): 获取 INSERT 关键字前的前缀,如INSERT INTO或者INSERT IGNORE
  • void setTable(Table table): 设置插入语句中的目标表。
  • void setColumns(List<Column> columns): 设置插入语句中要插入的列的列表。
  • void setValues(ItemsList values): 设置插入语句中的值列表。
  • void setPrefix(String prefix): 设置 INSERT 关键字前的前缀。

更新语句

Update 语句的处理方式与 Insert 类似,结构相对 Select 较为简单,通过 Update 对象即可获取相关内容:

String sql = "UPDATE employees SET department = 'Human Resources' WHERE employee_id = 1";

// Parse SQL
Statement statement = CCJSqlParserUtil.parse(sql);
if (statement instanceof Update) {
    Update updateStatement = (Update) statement;
    log.info("==> JSqlParser SQL: {}", updateStatement.toString());
    
    Table table = updateStatement.getTable();
    log.info("Table Name: {}", table.getName());
    log.info("==> Columns: {}", updateStatement.getColumns());
    
    // 获取更新项
    List<UpdateSet> updateSets = updateStatement.getUpdateSets();
    for (UpdateSet updateSet : updateSets) {
        for (Expression expression : updateSet.getColumns()) {
            log.info("==> Expression: {}", expression.toString());
        }
    }
    
    log.info("==> ItemsList: {}", updateStatement.getExpressions());
    Expression where = updateStatement.getWhere();
    log.info("==> Where: {}", where.toString());
}

运行结果:

==> JSqlParser SQL: UPDATE employees SET department = 'Human Resources' WHERE employee_id = 1
Table Name: employees
==> Columns: department
==> Expression: department
==> ItemsList: 'Human Resources'
==> Where: employee_id = 1

删除语句

String sql = "DELETE FROM table_name WHERE condition";
Statement statement = CCJSqlParserUtil.parse(sql);

if (statement instanceof Delete) {
    Delete deleteStatement = (Delete) statement;
    
    // 获取要删除的表
    Table table = deleteStatement.getTable();
    System.out.println("Table Name: " + table.getName());
    
    // 获取 WHERE 条件
    Expression where = deleteStatement.getWhere();
    System.out.println("Where Condition: " + where.toString());
}

运行结果:

Table Name: table_name
Where Condition: condition

从 SQL 语句中提取表名

Statement statement = CCJSqlParserUtil.parse("SELECT * FROM MY_TABLE1");
Select selectStatement = (Select) statement;
TablesNamesFinder tablesNamesFinder = new TablesNamesFinder();
List<String> tableList = tablesNamesFinder.getTableList(selectStatement);

最终 tableList 中将包含 SQL 语句中的所有表名,上述示例中仅有一个表名。

为 SQL 语句各个字段表达式添加别名

String sql = "SELECT id, name, nickname, age, job, department FROM staff_member WHERE nickname = '刘'";

// Parse SQL
Statement statement = CCJSqlParserUtil.parse(sql);
if (statement instanceof Select) {
    Select selectStatement = (Select) statement;
    final AddAliasesVisitor instance = new AddAliasesVisitor();
    instance.setPrefix("t");
    selectStatement.accept(instance);
    log.info("==> JSqlParser finalSQL: {}", selectStatement);
}

动态加字段加表达式加条件

使用 SelectUtils,为一个 Select 语句,增加查询的字段

Select select = (Select) CCJSqlParserUtil.parse("SELECT mydate FROM mytable");
SelectUtils.addExpression(select, new Column("mylocation"));

增加一个表达式

Select select = (Select) CCJSqlParserUtil.parse("SELECT a FROM mytable");
SelectUtils.addExpression(select, new Column("b"));
assertEquals("SELECT a, b FROM mytable", select.toString());

Addition add = new Addition();
add.setLeftExpression(new LongValue(5));
add.setRightExpression(new LongValue(6));
SelectUtils.addExpression(select, add);

assertEquals("SELECT a, b, 5 + 6 FROM mytable", select.toString());

增加一个 Join

动态添加 Join 时,可以为 Join 增加表达式,设置 Join 的表,并通过 setLeft()、setRight()、setInner() 方法设置 join 的方向,最终生成对应的 SQL 语句:

Select select = (Select) CCJSqlParserUtil.parse("SELECT a FROM mytable");
final EqualsTo equalsTo = new EqualsTo();
equalsTo.setLeftExpression(new Column("a"));
equalsTo.setRightExpression(new Column("b"));
Join addJoin = SelectUtils.addJoin(select, new Table("mytable2"), equalsTo);
addJoin.setLeft(true);
assertEquals("SELECT a FROM mytable LEFT JOIN mytable2 ON a = b", select.toString());

用 SelectUtils 构建一个 SQL 语句

以下是 SelectUtils 中的一些方法,不仅可为查询语句增加表达式、Join 和分组,还可使用 build 等方法构建 SQL 语句:

img

以下是一个构建查询语句的示例,其中使用了 addGroupBy 方法:

Select select = SelectUtils.buildSelectFromTableAndExpressions(new Table("mytable"),
        new Column("a"), new Column("b"));
SelectUtils.addExpression(select, new Column("c"));

final EqualsTo equalsTo = new EqualsTo();
equalsTo.setLeftExpression(new Column("id"));
equalsTo.setRightExpression(new Column("1"));
SelectUtils.addGroupBy(select, new Column("d"));

log.info("==> JSqlParser Build SQL: {}", select.toString());

输出结果:

==> JSqlParser Build SQL: SELECT a, b, c FROM mytable GROUP BY d

总结

上述代码示例较多,但核心使用方式很简单:直接调用CCJSqlParserUtil.parse(sql)获取 Statement 对象,然后通过 Statement 操作和获取解析后的 SQL 内容,使用非常便捷。

实际应用场景

SQL 是 B/S 软件的核心组成部分,针对 SQL 处理有很多应用场景,以下列举几个常见的使用场景:

  • SQL 审计和分析
    • 审计 SQL 语句,检查是否包含潜在的安全漏洞,如 SQL 注入。
    • 分析 SQL 语句的性能,检查是否存在可以优化的查询条件。
  • 数据库迁移和同步
    • 在迁移数据库时,使用 JSqlParser 解析源数据库的 SQL 语句,并生成目标数据库的相应语句。
    • 数据库同步工具可以使用 JSqlParser 来解析和生成 SQL 语句,以实现数据的同步。
  • 动态 SQL 生成
    • 应用程序需要生成动态 SQL 语句以执行不同的操作,JSqlParser 可以用来解析这些动态生成的 SQL 语句。
  • SQL 测试和验证
    • 在开发过程中,使用 JSqlParser 来验证 SQL 语句的正确性。
    • 单元测试中,使用 JSqlParser 来解析和执行测试用例中的 SQL 语句。
  • SQL 注入防护
    • 在应用程序中,使用 JSqlParser 来解析和分析用户输入的 SQL 查询,以防止 SQL 注入攻击。
  • 数据库管理工具
    • 数据库管理工具可以使用 JSqlParser 来解析和显示 SQL 语句的结构,帮助开发者理解查询的逻辑。
  • 代码生成
    • 在生成数据库访问层代码时,使用 JSqlParser 来解析 SQL 语句,并生成相应的数据访问对象(DAO)或查询对象(DTO)。
  • SQL 格式化
    • 使用 JSqlParser 来格式化 SQL 语句,使其更易于阅读和理解。
  • SQL 优化
    • 通过分析 SQL 语句的结构,可以提出性能优化建议。
  • 数据处理工具
    • 在数据处理和转换工具中,使用 JSqlParser 来解析和生成 SQL 语句,以实现数据的导入和导出。

高级特性

JSqlParser 在解析 SQL 语句时,每个节点都会被解析为 SimpleNode 对象,包含各个节点的属性。这与 Dom4j 解析 XML 时将元素视为 Node 类似,解析后的内容都是节点。JSqlParser 提供了遍历节点的接口CCJSqlParserVisitor,其默认实现为CCJSqlParserDefaultVisitor。通过创建自定义类继承CCJSqlParserDefaultVisitor并重写 visit 方法,可实现自定义策略,更方便地操作解析内容:

public class SQLModifier extends CCJSqlParserDefaultVisitor {
    @Override
    public Object visit(SimpleNode node, Object data) {
        Object value = node.jjtGetValue();
        switch (node.getId()) {
            case CCJSqlParserTreeConstants.JJTTABLENAME:
                break;
            case CCJSqlParserTreeConstants.JJTCOLUMN:
                break;
            case CCJSqlParserTreeConstants.JJTFUNCTION:
                break;
            default:
                break;
        }
        return super.visit(node, data);
    }
}

调用自定义的 Visitor

String originalSql = "SELECT * FROM user WHERE id = 1";
CCJSqlParser parser = CCJSqlParserUtil.newParser(originalSql);
Statement statement = parser.Statement();
parser.getASTRoot().jjtAccept(sqlTestModifier, null);

上述代码创建了一个自定义的 visitor,重写的 visit 方法接收 SimpleNode 参数。调用自定义 Visitor 后,语句会被拆解并依次进入 visit 方法。通过node.jjtGetValue()获取节点信息,node.getId()获取节点类型。Switch-case 中的常量代表解析 SQL 语句时生成的抽象语法树 AST(Abstract Syntax Tree)中不同类型的节点,每个节点对应特定的 SQL 构造,如 SELECT、FROM、WHERE 等。以下是对这些常量代表的 SQL 构造的简要说明:

  • JJTSTATEMENT: 代表一个 SQL 语句。
  • JJTVOID: 可能代表一个空语句或者不返回结果的语句。
  • JJTBLOCK: 代表一个语句块,可能包含多个语句。
  • JJTSTATEMENTS: 代表一个包含多个语句的列表。
  • JJTCOLUMN: 代表一个列名。
  • JJTTABLENAME: 代表一个表名。
  • JJTSELECT: 代表一个 SELECT 查询。
  • JJTPARENTHESEDSELECT: 代表被括号包围的 SELECT 查询。
  • JJTLATERALVIEW: 代表 LATERAL VIEW 子句,常用于 Hive SQL。
  • JJTFORCLAUSE: 代表 FOR 子句。
  • JJTLATERALSUBSELECT: 代表 LATERAL 子查询。
  • JJTPLAINSELECT: 代表一个简单的 SELECT 查询(不包含 UNION 等)。
  • JJTSETOPERATIONLIST: 代表一个集合操作列表,比如 UNION, EXCEPT, INTERSECT。
  • JJTWITHITEM: 代表 WITH 子句中的单个项。
  • JJTSELECTITEM: 代表 SELECT 子句中的一个项,可能是列名、表达式等。
  • JJTJOINEREXPRESSION: 代表 JOIN 操作的表达式。
  • JJTLIMITWITHOFFSET: 代表 LIMIT 和 OFFSET 子句。
  • JJTPLAINLIMIT: 代表一个简单的 LIMIT 子句。
  • JJTEXPRESSION: 代表一个表达式。
  • JJTREGULARCONDITION: 代表一个常规条件(如 WHERE 子句中的条件)。
  • JJTINEXPRESSION: 代表 IN 表达式。
  • JJTLIKEEXPRESSION: 代表 LIKE 表达式。
  • JJTSIMILARTOEXPRESSION: 代表 SIMILAR TO 表达式。
  • JJTISDISTINCTEXPRESSION: 代表 IS DISTINCT FROM 表达式。
  • JJTEXPRESSIONLIST: 代表一个表达式列表。
  • JJTPRIMARYEXPRESSION: 代表一个主要表达式。
  • JJTCONNECTBYROOTOPERATOR: 代表 CONNECT BY ROOT 操作符。
  • JJTCASEWHENEXPRESSION: 代表 CASE WHEN 表达式。
  • JJTFUNCTION: 代表一个函数调用。
  • JJTSEQUENCE: 代表一个序列。
  • JJTSYNONYM: 代表一个同义词。

Visit 常见应用场景

通过 MyBatis 的 interceptor 可以拦截所有执行的 SQL 语句,在自定义 interceptor 中调用自定义的 visit 方法,可以对项目中所有运行的 SQL 进行拦截和处理。以下是一些具体的应用场景:

  1. SQL 语句重写:在某些数据库系统中,为了优化性能或满足特定的需求,可能需要重写 SQL 语句。通过自定义访问者,可以在 AST(Abstract Syntax Tree)层面进行这些操作。
  2. 元数据提取:自定义访问者可以用来提取 SQL 语句中的元数据,比如查询涉及的所有表名、列名、函数等,这些信息可以用于构建数据库的概要图或进行数据治理。
  3. 数据屏蔽:在需要对敏感数据进行屏蔽的应用中,可以通过自定义访问者来识别并修改涉及敏感数据的查询,以确保在查询结果中不会暴露敏感信息。
  4. 动态查询构建:在需要动态构建 SQL 查询的应用中,可以通过自定义访问者来解析模板 SQL 语句,并根据实际参数动态替换模板中的占位符,从而构建出完整的 SQL 语句。
  5. 缓存策略决策:根据 SQL 查询的特征,可以通过自定义访问者来判断查询结果是否适合缓存,以及应该使用什么样的缓存策略。

总结

JSqlParser 使用简单,有效解决了 SQL 语句解析的问题。结合 SpringBoot 和 MyBatis,可以设计自定义插件,类似于 MyBatis Plus 的分页插件,开发满足系统需求的业务处理功能,提高项目开发效率。

posted @ 2025-07-05 16:36  Higurashi-kagome  阅读(1099)  评论(0)    收藏  举报