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 核心功能
- 表名收集:自动遍历 SQL 语句的各个部分,收集所有引用的表名到内部集合中
- 可配置性:通过
init()
方法可以配置是否处理表的别名 - 便捷访问:提供
getTableList()
等方法,方便获取收集到的表名信息 - 可扩展性:允许子类重写特定的 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 语句中的列添加表名前缀或别名:
/*** 规范化列访问器,继承自 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);}@Overridepublic 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);}@Overridepublic 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方法实现...@Overridepublic 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 方法
/*** 创建一个用于获取表别名的函数* 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;}@Overridepublic 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 方法
/*** 遍历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 方法
/*** 获取 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 语句的示例:
/*** 规范化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 语句中的列添加表名前缀。
输入输出示例
输入:
SELECT id, name FROM user WHERE age > 18 ORDER BY create_time;
输出(使用 user 作为表名前缀):
SELECT user.id, user.name FROM user WHERE user.age > 18 ORDER BY user.create_time;
6.2 SQL 安全增强
基于 TablesNamesFinder,我们可以实现 SQL 安全增强功能,如自动为 SQL 添加访问控制条件:
/*** 为 SQL 语句添加访问限制条件*/
private static Statement addLimitForStatement(Statement statement, String joinClause, String limitExpression) {statement.accept(new TablesNamesFinder(){{init(true);}@Overridepublic void visit(PlainSelect plainSelect) {super.visit(plainSelect);doVisitForAddLimit(limitExpression, joinClause, plainSelect::getWhere, plainSelect::getJoins, plainSelect::setWhere, plainSelect::setJoins);}@Overridepublic void visit(Delete delete) {super.visit(delete);doVisitForAddLimit(limitExpression, joinClause, delete::getWhere, delete::getJoins, delete::setWhere, delete::setJoins);}@Overridepublic 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 的格式化和规范化处理。
相对于通用的 CCJSqlParserVisitor
,TablesNamesFinder
使用更加简单便捷,特别适合于需要对 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 格式化实现》