Java实现SQL解析案例

wen java案例 3

从零到一:Java实现SQL解析的完整实战案例与核心原理剖析


目录导读

  1. 为什么你需要自己解析SQL?
  2. 主流Java SQL解析库横评:JSqlParser vs Druid vs Calcite
  3. 手写一个轻量级SQL解析器:词法分析(Token化)
  4. 语法分析实战:构建AST(抽象语法树)
  5. 完整案例:解析SELECT语句并提取表名与过滤条件
  6. 进阶问答:如何处理复杂JOIN与子查询?
  7. 性能优化与注意事项
  8. 拥抱或规避,你的选择

为什么你需要自己解析SQL?

在业务系统开发中,我们常遇到以下痛点:

Java实现SQL解析案例

  • 数据权限管控:需要动态拦截用户SQL,强制追加WHERE tenant_id = ?
  • 血缘分析:解析SQL以追踪数据流向,用于治理或推荐。
  • 多方言支持:统一不同数据库(MySQL、Oracle、PG)的SQL语法差异。

直接使用正则表达式解析SQL是“灾难”,因为SQL语法复杂且存在嵌套层级。健壮的做法是构建AST(抽象语法树)——将SQL拆解为结构化树形节点,便于遍历和修改。


主流Java SQL解析库横评

库名称 特点 适用场景
JSqlParser 轻量、易用,基于JavaCC生成解析器 快速提取表名、列名,改写简单条件
Apache Calcite 重量级,支持查询优化、多数据源联邦 大数据平台(如Flink、Hive)的SQL层
Druid SQL Parser 阿里开源,兼容多种方言,性能极佳 中间件(如MyCat)的SQL路由与改写

选型建议:若仅需解析单条SQL,优先选JSqlParser;若需深度优化与SQL标准支持,选Calcite,本文案例使用JSqlParser演示(因为其API最直观),但会讲解其内部原理。


手写一个轻量级SQL解析器:词法分析(Token化)

任务:将字符串SELECT id, name FROM users WHERE age > 18拆分为Token序列。

public enum TokenType { KEYWORD, IDENTIFIER, NUMBER, OPERATOR, COMMA, DOT, PAREN_OPEN, PAREN_CLOSE }
public class Token {
    TokenType type;
    String value;
}
public List<Token> tokenize(String sql) {
    List<Token> tokens = new ArrayList<>();
    // 定义正则:匹配关键词(SELECT/FROM/WHERE)、标识符(字母/下划线开头)、数字、比较运算符
    // 省略具体正则实现,核心是逐字符扫描并匹配规则
    return tokens;
}

输出[SELECT(KEYWORD), id(IDENTIFIER), COMMA, name(IDENTIFIER), FROM(KEYWORD), users(IDENTIFIER), WHERE(KEYWORD), age(IDENTIFIER), >(OPERATOR), 18(NUMBER)]


语法分析实战:构建AST

语法分析依据语法规则(产生式)将Token序列组合成树。

  • SELECT <列列表> FROM <表名> WHERE <条件> 对应一个SelectStatement节点。
  • <条件> 进一步分解为ComparisonExpression(左值、操作符、右值)。

JSqlParser的AST节点示例(通过其API获取):

Statement statement = CCJSqlParserUtil.parse(sql);
if (statement instanceof Select) {
    PlainSelect plainSelect = (PlainSelect) ((Select) statement).getSelectBody();
    List<String> columns = plainSelect.getSelectItems().stream()
            .map(item -> item.toString()).collect(Collectors.toList());
    String tableName = plainSelect.getFromItem().toString();
    Expression where = plainSelect.getWhere();
}

完整案例:解析SELECT语句并提取表名与过滤条件

需求:给定SQL,输出:表名 = users过滤条件 = {age > 18},并支持追加AND deleted = 0

代码实现(JSqlParser)

private static void parseAndModify(String sql) throws JSQLParserException {
    Statement stmt = CCJSqlParserUtil.parse(sql);
    if (stmt instanceof Select) {
        PlainSelect select = (PlainSelect) ((Select) stmt).getSelectBody();
        // 提取表名
        String table = select.getFromItem().toString();
        System.out.println("原始表名: " + table);
        // 提取WHERE条件
        Expression where = select.getWhere();
        System.out.println("原始条件: " + where);
        // 追加条件 (使用EqualsTo拼接)
        EqualsTo newCondition = new EqualsTo();
        newCondition.setLeftExpression(new Column("deleted"));
        newCondition.setRightExpression(new LongValue(0));
        if (where == null) {
            select.setWhere(newCondition);
        } else {
            AndExpression andExpr = new AndExpression(where, newCondition);
            select.setWhere(andExpr);
        }
        System.out.println("修改后SQL: " + select.toString());
    }
}

运行结果

原始表名: users
原始条件: age > 18
修改后SQL: SELECT id, name FROM users WHERE age > 18 AND deleted = 0

进阶问答:如何处理复杂JOIN与子查询?

Q1:如何获取JOIN语句的表名?
A:使用Join对象列表。PlainSelect.getJoins()返回List<Join>,每个Join对象有getRightItem()(表名)和getOnExpression()(连接条件)。

Q2:子查询嵌套如何遍历?
A:JSqlParser将子查询作为SubSelect节点,需要递归遍历:

if (fromItem instanceof SubSelect) {
    PlainSelect subSelect = (PlainSelect) ((SubSelect) fromItem).getSelectBody();
    // 递归处理
}

Q3:能否解析INSERT/UPDATE/DELETE
A:可以。CCJSqlParserUtil.parse()返回的Statement可能是UpdateInsert等类型,结构略有不同,但思路一致。


性能优化与注意事项

  • 缓存解析结果:SQL语法树解析较耗CPU(毫秒级),对于高频重复SQL(如固定报表),建议使用ConcurrentHashMap缓存AST。
  • 版本兼容:JSqlParser不同版本的API略有差异,如getSelectBody()在4.x版本才稳定,建议固定版本。
  • SQL注入防御:若用于解析用户输入,务必校验Token类型,防止恶意构造的Token(如多行注释)绕过过滤。
  • 方言差异:JSqlParser支持部分方言,但复杂函数(如MySQL的GROUP_CONCAT)可能解析失败,需自定义Function节点。

拥抱或规避,你的选择

SQL解析是数据中台、低代码平台、数据库工具开发的基石,通过本文案例,你已掌握:

  • 词法分析与语法分析的基本原理。
  • 使用JSqlParser快速解析和改写SQL。
  • 应对JOIN、子查询等复杂结构的策略。

行动建议

  • 如果只是简单提取表名,直接引入JSqlParser依赖。
  • 如果你需要深度优化(如谓词下推),则必须学习Apache Calcite的RelNode体系。
  • 别忘了测试极端情况(如字符串中的WHERE关键字、嵌套注释)。

问题思考:当SQL中出现ALTER TABLE语句时,如何通过AST判断操作类型(添加列 vs 删除列)?欢迎在评论区给出你的思路。

抱歉,评论功能暂时关闭!