在數據庫應用開發中,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 格式化實現》