jsqlparser(六):TablesNamesFinder 深度解析与 SQL 格式化实现

在数据库应用开发中,SQL语句的解析和处理是一项常见而重要的任务。本文将深入探讨 JSQLParser 中的 TablesNamesFinder 类,分析其核心原理、与 AST 访问接口(CCJSqlParserVisitor )的关系、使用场景,并通过实际代码示例展示如何基于 TablesNamesFinder 实现 SQL 语句的格式化处理。

一、JSQLParser AST 访问机制概述

JSQLParser 采用访问者模式(Visitor Pattern)来处理解析后的 SQL 抽象语法树(AST)。在这个设计中,有几个核心组件:

1.1 核心访问接口

CCJSqlParserVisitor 是 JSQLParser 中定义的一个关键接口,它是 SQL 抽象语法树(AST)的访问接口,为访问各种 SQL 语法元素提供了统一的方法。此外,JSQLParser 还提供了一系列更具体的访问接口:

  • StatementVisitor:用于访问不同类型的 SQL 语句(如 SELECT、UPDATE、DELETE 等)
  • ExpressionVisitor:用于访问各种表达式(如条件表达式、算术表达式等)
  • SelectVisitor:专门用于访问 SELECT 语句的各个部分
  • FromItemVisitor:用于访问 FROM 子句中的各种表引用
  • SelectItemVisitor:用于访问 SELECT 列表中的各个项目
  • ItemsListVisitor:用于访问项目列表(如 IN 表达式中的值列表)

1.2 访问者实现体系

为了简化开发,JSQLParser 提供了多个适配器类,实现了上述接口的所有方法(通常为空实现),开发者可以继承这些适配器,只重写自己关心的方法。

二、TablesNamesFinder 核心原理

TablesNamesFinder 是 JSQLParser 库中提供的一个实用工具类,它的核心功能是遍历解析后的 SQL 语句或子句对象(Select,Where...)的所有节点并收集其中引用的所有表名,并提供通过重写访问者方法来扩展或定制 SQL 处理逻辑的能力。

2.1 类的继承关系

TablesNamesFinder 通过继承一系列适配器类来实现其功能,而不是直接实现 CCJSqlParserVisitor 接口。它的主要继承路径为:

复制代码
// TablesNamesFinder 继承了多个访问者接口,用于处理不同类型的 SQL 元素
StatementVisitor, ExpressionVisitor, SelectVisitor, FromItemVisitor, SelectItemVisitor, ItemsListVisitor <-- TablesNamesFinder

2.2 核心功能

  1. 表名收集:自动遍历 SQL 语句的各个部分,收集所有引用的表名到内部集合中
  2. 可配置性 :通过 init() 方法可以配置是否处理表的别名
  3. 便捷访问 :提供 getTableList() 等方法,方便获取收集到的表名信息
  4. 可扩展性:允许子类重写特定的 visit 方法,实现自定义的 SQL 处理逻辑

三、TablesNamesFinder 与 CCJSqlParserVisitor 的关系与区别

特性 TablesNamesFinder CCJSqlParserVisitor
类型 具体工具类 SQL抽象语法树(AST)核心接口
设计目的 用于收集SQL语句中的表名, 更重要的是提供了定制化的处理能力 定义访问SQL抽象语法树的标准方法
接口实现 通过实现不同节点的访问者接口, 遍历所有的SQL语法元素(比如 Table,Column,Expression) 顶层接口,定义访问各种SQL语法原始元素的方法
使用场景 提取SQL中使用的表、分析表依赖关系 作为实现自定义SQL处理逻辑的基础接口
执行阶段 在SQL解析为语法对象上执行 SQL解析阶段被调用

四、TablesNamesFinder 使用场景

  • SQL 表依赖分析

通过 TablesNamesFinder 可以快速提取 SQL 语句中引用的所有表名,用于分析 SQL 的表依赖关系,这在数据库迁移、表结构变更影响分析等场景中非常有用。

  • SQL 安全性检查

可以基于 TablesNamesFinder 构建安全检查器,识别 SQL 中是否引用了敏感表,或是否包含未授权访问的表。

  • SQL 格式化和规范化

通过扩展 TablesNamesFinder,可以实现 SQL 语句的格式化和规范化,例如为列名添加表名前缀、标准化表别名等。

  • SQL 重构和转换

基于 TablesNamesFinder 可以实现 SQL 的重构和转换,如自动添加 WHERE 条件、替换表名等。

五、基于 TablesNamesFinder 实现 SQL 格式化

下面,我们通过一个实际的代码示例来展示如何基于 TablesNamesFinder 实现 SQL 语句的格式化。

5.1 代码实现

以下是一个名为 CanonicalColumnVisitor 的内部类,它继承自 TablesNamesFinder,用于为 SQL 语句中的列添加表名前缀或别名:

java 复制代码
/**
 * 规范化列访问器,继承自 TablesNamesFinder,用于为 SQL 语句中的列添加表名前缀或别名。
 * 该访问器会遍历 SQL 语句中的各个部分,包括 WHERE 子句、JOIN 条件、排序和分组等,
 * 为缺少表名前缀的列添加指定的表名或其别名,同时为表添加合适的别名。
 * 在遍历过程中,还会收集关联的表信息。
 */
private static class CanonicalColumnVisitor extends TablesNamesFinder {
    private final String tablename;
    private final Map<String,FromItem> associatedTables = new HashMap<>();
    private final Function<String,String> aliaFunction = asAliasFunction(associatedTables);

    /**
     * 构造CanonicalColumnVisitor实例,关联表映射使用null值,适用于对完整SQL语句的处理
     * @param tablename 表名,用于为列名添加表名前缀
     */
    CanonicalColumnVisitor(String tablename) {
        this(tablename, null);
    }

    /**
     * 构造CanonicalColumnVisitor实例
     * @param tablename 表名,用于为列名添加表名前缀
     * @param joinedTables 已连接的表映射,键为表名,值为对应的FromItem对象
     */
    CanonicalColumnVisitor(String tablename, Map<String,FromItem> joinedTables) {
        this.tablename = tablename;
        if(null != joinedTables && !joinedTables.isEmpty()){
            this.associatedTables.putAll(joinedTables);
        }
        init(true);
    }

    @Override
    public void visit(Column column) {
        /** 为列名增加表名前缀,优先使用表的别名 */
        Table table = column.getTable();
        if (!isNullOrEmpty(tablename)) {
            if (null == table) {
                String aliasName = aliaFunction.apply(tablename);
                column.setTable(new Table(null != aliasName ? aliasName : tablename));
            }
        }
        if (null != table) {
            Alias alias = table.getAlias();
            if (null == alias) {
                String aliasName = aliaFunction.apply(table.getName());
                if (null != aliasName && !aliasName.equals(table.getName())) {
                    alias = new Alias(aliasName);
                    table.setAlias(alias);
                }
            }
        }
        super.visit(column);
    }

    @Override
    public void visit(PlainSelect plainSelect) {
        doVisitForCollectAssociatedTable(associatedTables, plainSelect.getFromItem(), plainSelect.getJoins());
        super.visit(plainSelect);
        // 处理JOIN条件、WHERE子句、ORDER BY和GROUP BY等部分
        if (plainSelect.getJoins() != null) {
            for (Join join : plainSelect.getJoins()) {
                for(Expression exp: join.getOnExpressions()) {
                    exp.accept(this);
                }
            }
        }
        if (plainSelect.getWhere() != null) {
            plainSelect.getWhere().accept(this);
        }
        if (plainSelect.getOrderByElements() != null) {
            for (OrderByElement item : plainSelect.getOrderByElements()) {
                item.getExpression().accept(this);
            }
        }
        if (plainSelect.getGroupBy() != null) {
            plainSelect.getGroupBy().getGroupByExpressionList().accept(this);
        }
    }

    // 其他SQL语句类型的visit方法实现...
    @Override
    public void visit(Update update) {
        doVisitForCollectAssociatedTable(associatedTables, update.getTable(), update.getJoins());
        super.visit(update);
        // 处理UPDATE语句的更新列和表达式
        update.getUpdateSets().forEach(us -> {
            us.getColumns().forEach(c -> c.accept(this));
            us.getExpressions().forEach(e -> e.accept(this));
        });
    }

    // Delete、Upsert等其他语句类型的visit方法实现...
}

5.2 关键辅助方法

该实现中使用了几个关键的辅助方法:

5.2.1 asAliasFunction 方法
java 复制代码
/**
 * 创建一个用于获取表别名的函数
 * 1. 根据输入的表名,从已JOIN表映射中查找对应的FromItem对象
 * 2. 若找到FromItem且有别名,则返回别名
 * 3. 若找到FromItem但无别名,则返回原表名
 * 4. 若未找到FromItem,则返回null
 */
private static Function<String, String> asAliasFunction(Map<String, FromItem> joinedTables) {
    class AliasFunction implements Function<String, String> {
        private final Map<String, FromItem> joinedTables;
        
        AliasFunction(Map<String, FromItem> joinedTables) {
            this.joinedTables = null == joinedTables ? Collections.emptyMap() : joinedTables;
        }
        
        @Override
        public String apply(String name) {
            FromItem fromItem = null == name ? null : joinedTables.get(name);
            if (null == fromItem) {
                return null;
            }
            Alias alias = fromItem.getAlias();
            return (null == alias || alias.getName() == null) ? name : alias.getName();
        }
        
        // hashCode、equals和toString方法实现...
    }
    return new AliasFunction(joinedTables);
}
5.2.2 doVisitForCollectAssociatedTable 方法
java 复制代码
/**
 * 遍历FromItem和JOIN子句,将其中的表信息添加到已连接表映射中
 */
private static void doVisitForCollectAssociatedTable(Map<String, FromItem> joinedTables, 
                                                    FromItem fromItem, List<Join> joins) {
    if(null != fromItem) {
        joinedTables.put(tablenameOrAliasOf(fromItem), fromItem);
    }
    if(null != joins){
        joins.stream()
            .map(j -> j.getRightItem())
            .forEach(i->joinedTables.put(tablenameOrAliasOf(i), i));
    }
}
5.2.3 tablenameOrAliasOf 方法
java 复制代码
/**
 * 获取 FromItem 对象对应的表名或别名
 */
private static String tablenameOrAliasOf(FromItem fromItem) {
    if(null == fromItem) {
        return null;
    }
    if(fromItem instanceof Table) {
        return ((Table)fromItem).getName();
    }
    Alias alias = fromItem.getAlias();
    return (null == alias) ? null : alias.getName();
}

5.3 使用示例

以下是如何使用 CanonicalColumnVisitor 来规范化 SQL 语句的示例:

java 复制代码
/**
 * 规范化SQL语句对应的Statement对象
 * 解析传入的SQL语句,获取对应的Statement对象,并使用CanonicalColumnVisitor对其进行访问,
 * 为没有指定表名的字段名自动加上 tablename 指定的表名前缀
 */
private static Statement normalizeStatement(String tablename, String sql) throws JSQLParserException {
    Statement statement = parseStatement(sql);
    return normalizeStatement(tablename, statement);
}

/**
 * 规范化SQL语句对应的Statement对象
 * 使用CanonicalColumnVisitor对传入的Statement对象进行访问,
 * 为没有指定表名的字段名自动加上 tablename 指定的表名前缀
 */
private static Statement normalizeStatement(String tablename, Statement statement) throws JSQLParserException {
    statement.accept(new CanonicalColumnVisitor(tablename));
    return statement;
}

/**
 * 解析 SQL 语句字符串并返回对应的 Statement 对象
 */
private static Statement parseStatement(String sql) throws JSQLParserException {
    // 解析SQL语句的实现
    return ParserSupport.parse0(sql, null, null).statement;
}

/**
 * 规范化SQL字符串
 */
private static String normalizeSql(String tablename, String sql) {
    if(isNullOrEmpty(sql)){
        return sql;
    }
    try {
        return normalizeStatement(tablename, sql).toString();
    } catch (JSQLParserException e) {
        // 解析SQL语句失败,不做任何处理返回原值
        return sql;
    }
}

六、实际应用场景分析

6.1 SQL 格式化与规范化

在多表联合查询中,为每个列添加表名前缀可以避免列名歧义,提高 SQL 语句的可读性和可维护性。通过 CanonicalColumnVisitor,我们可以自动为 SQL 语句中的列添加表名前缀。

输入输出示例

输入:

sql 复制代码
SELECT id, name FROM user WHERE age > 18 ORDER BY create_time;

输出(使用 user 作为表名前缀):

sql 复制代码
SELECT user.id, user.name FROM user WHERE user.age > 18 ORDER BY user.create_time;

6.2 SQL 安全增强

基于 TablesNamesFinder,我们可以实现 SQL 安全增强功能,如自动为 SQL 添加访问控制条件:

java 复制代码
/**
 * 为 SQL 语句添加访问限制条件
 */
private static Statement addLimitForStatement(Statement statement, String joinClause, String limitExpression) {
    statement.accept(new TablesNamesFinder(){
        {
            init(true);
        }
        @Override
        public void visit(PlainSelect plainSelect) {
            super.visit(plainSelect);
            doVisitForAddLimit(limitExpression, joinClause, 
                    plainSelect::getWhere, plainSelect::getJoins, 
                    plainSelect::setWhere, plainSelect::setJoins);
        }

        @Override
        public void visit(Delete delete) {
            super.visit(delete);
            doVisitForAddLimit(limitExpression, joinClause, 
                    delete::getWhere, delete::getJoins, 
                    delete::setWhere, delete::setJoins);
        }

        @Override
        public void visit(Update update) {
            super.visit(update);
            doVisitForAddLimit(limitExpression, joinClause, 
                    update::getWhere, update::getJoins, 
                    update::setWhere, update::setJoins);
        }
    });

    return statement;
}

七、总结

TablesNamesFinder 是 JSQLParser 提供的一个强大工具类,通过继承和扩展它,我们可以实现各种复杂的 SQL 处理功能。本文通过实际代码示例,展示了如何基于 TablesNamesFinder 实现 SQL 的格式化和规范化处理。

相对于通用的 CCJSqlParserVisitorTablesNamesFinder 使用更加简单便捷,特别适合于需要对 SQL 表引用进行分析和处理的场景。

在实际应用中,我们可以根据具体需求,进一步扩展 TablesNamesFinder,实现更复杂的 SQL 处理逻辑,如 SQL 重构、SQL 优化建议、SQL 安全检查等功能。

jsqlparser系列文章

《jsqlparser(一):基于抽象语法树(AST)遍历SQL语句的语法元素》
《jsqlparser(二):实现基于SQL语法分析的SQL注入攻击检查》
《jsqlparser(三):基于语法分析实现SQL中的CAST函数替换》
《jsqlparser(四):实现MySQL 函数DATE_FORMAT到Phoenix函数TO_CHAR的替换》
《jsqlparser(五):修改语法定义(JSqlParserCC.jjt)实现UPSERT支持Phoenix语法ON DUPLICATE KEY IGNORE》
《jsqlparser(六):TablesNamesFinder 深度解析与 SQL 格式化实现》

相关推荐
CYRUS_STUDIO2 小时前
一步步带你移植 FART 到 Android 10,实现自动化脱壳
android·java·逆向
2301_803554522 小时前
mysql(自写)
数据库·mysql
练习时长一年2 小时前
Spring代理的特点
java·前端·spring
CYRUS_STUDIO2 小时前
FART 主动调用组件深度解析:破解 ART 下函数抽取壳的终极武器
android·java·逆向
MisterZhang6663 小时前
Java使用apache.commons.math3的DBSCAN实现自动聚类
java·人工智能·机器学习·自然语言处理·nlp·聚类
麦麦大数据3 小时前
vue+Django 双推荐算法旅游大数据可视化系统Echarts mysql数据库 带爬虫
数据库·vue.js·django·可视化·推荐算法·百度地图·旅游景点
成都极云科技3 小时前
裸金属服务器与虚拟机、物理机的核心差异是什么?
运维·服务器·数据库
学习中的程序媛~3 小时前
图数据库neo4j的安装
数据库·neo4j
Swift社区3 小时前
Java 常见异常系列:ClassNotFoundException 类找不到
java·开发语言
喂完待续4 小时前
【Big Data】AI赋能的ClickHouse 2.0:从JIT编译到LLM查询优化,下一代OLAP引擎进化路径
大数据·数据库·clickhouse·数据分析·olap·big data·序列晋升