在数据库应用开发中,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 语句中的列添加表名前缀或别名:
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 的格式化和规范化处理。
相对于通用的 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 格式化实现》