Appearance
第三部分:精通篇 - 高级应用与技巧 (Expert)
模块五:使用 Visitor 模式遍历和修改 SQL
到目前为止,我们主要通过直接调用 JSqlParser 解析后对象(如 PlainSelect, Column, Function 等)的 get 方法来访问 SQL 语句的各个部分。当 SQL 结构变得复杂,或者我们需要对 AST (抽象语法树) 进行系统性的遍历或修改时,这种直接访问的方式会变得非常繁琐且容易出错。
Visitor 设计模式 提供了一种更优雅、更灵活的方式来处理复杂的对象结构(如 JSqlParser 的 AST)。
1. Visitor 设计模式简介
1.1 为什么需要 Visitor 模式?
想象一下,你需要对一个复杂的 SQL AST 执行多种不同的操作:
- 操作1: 收集所有用到的表名。
- 操作2: 收集所有用到的列名。
- 操作3: 找到所有类型为 VARCHAR 的列定义。
- 操作4: 将所有表名转换为大写。
- 操作5: 在所有 SELECT 语句的 WHERE 条件中添加一个 tenant_id = 'xxx' 的过滤。
如果我们将这些操作逻辑直接写在 AST 节点类(如 Table, Column, Select)中,会导致这些类变得非常臃肿,并且每次增加新的操作都需要修改这些核心类,违反了“开闭原则”(对扩展开放,对修改关闭)。
Visitor 模式通过以下方式解决了这个问题:
- 将操作与对象结构分离: 它允许你在不修改对象结构(AST 节点类)的前提下,定义新的操作。
- 双重分发 (Double Dispatch): Visitor 模式的核心机制。当一个 Visitor 访问一个元素时:
- 元素调用 Visitor 的特定方法,并将自身 (element) 作为参数传递过去 (element.accept(visitor))。
- 在 Visitor 的方法内部,通常会回调元素的一个特定方法,并将 Visitor 自身作为参数 (visitor.visit(this_specific_element_type))。这样,Visitor 就知道当前正在访问的具体元素类型,从而执行相应的操作。
1.2 Visitor 模式的核心思想
- Visitor (访问者) 接口/抽象类: 定义了一系列 visit() 方法,每个方法对应 AST 中一种具体类型的节点 (Element)。例如,visit(Table table), visit(Column column), visit(Select select)。
- ConcreteVisitor (具体访问者) 类: 实现 Visitor 接口,为每个 visit() 方法提供具体的业务逻辑。不同的 ConcreteVisitor 可以实现不同的操作(如收集表名、修改列名等)。
- Element (元素) 接口/抽象类: 定义了一个 accept(Visitor visitor) 方法,接收一个 Visitor 对象作为参数。
- ConcreteElement (具体元素) 类: 实现 Element 接口。其 accept() 方法通常会调用 Visitor 对应当前元素类型的 visit() 方法,例如 visitor.visit(this)。
在 JSqlParser 中,Statement, Expression, FromItem 等接口和它们的实现类扮演了 Element 的角色,而 JSqlParser 提供了一系列 Visitor 接口供我们实现。
2. JSqlParser 中的 Visitor 接口
JSqlParser 为其 AST 的不同部分提供了专门的 Visitor 接口。通常,每个主要的 AST 节点接口都有一个对应的 Visitor 接口。为了方便使用,JSqlParser 还为每个 Visitor 接口提供了一个默认的适配器类 (Adapter),该适配器类为 Visitor 接口中的所有方法提供了空实现。这样,我们在创建自定义 Visitor 时,只需要继承适配器类并覆盖我们感兴趣的节点类型的 visit() 方法即可,而无需实现所有方法。
以下是一些核心的 Visitor 接口及其对应的适配器:
- StatementVisitor / StatementVisitorAdapter:
用于访问各种类型的 SQL 语句 (Statement 的实现类)。- visit(Select select)
- visit(Insert insert)
- visit(Update update)
- visit(Delete delete)
- visit(CreateTable createTable)
- visit(Drop drop)
- visit(Alter alter)
- visit(Truncate truncate)
- ... 等等
- SelectVisitor / SelectVisitorAdapter:
用于访问 SelectBody 的不同实现。- visit(PlainSelect plainSelect)
- visit(SetOperationList setOpList) (用于 UNION, INTERSECT 等)
- visit(ValuesStatement valuesStatement)
- ExpressionVisitor / ExpressionVisitorAdapter:
这是最常用和最复杂的 Visitor 之一,用于访问各种类型的表达式 (Expression 的实现类)。- visit(Column tableColumn)
- visit(StringValue value)
- visit(LongValue value)
- visit(DoubleValue value)
- visit(DateValue value)
- visit(TimestampValue value)
- visit(NullValue value)
- visit(Function function)
- visit(SubSelect subSelect)
- visit(AndExpression andExpression)
- visit(OrExpression orExpression)
- visit(EqualsTo equalsTo)
- visit(GreaterThan greaterThan)
- visit(LikeExpression likeExpression)
- visit(InExpression inExpression)
- visit(Between between)
- visit(CaseExpression caseExpression)
- visit(WhenClause whenClause)
- ... 等等,几乎每种表达式都有对应的 visit 方法。
- FromItemVisitor / FromItemVisitorAdapter:
用于访问 FROM 子句中的项 (FromItem 的实现类)。- visit(Table tableName)
- visit(SubSelect subSelect)
- visit(SubJoin subjoin)
- visit(LateralSubSelect lateralSubSelect)
- ItemsListVisitor / ItemsListVisitorAdapter:
用于访问 INSERT 语句中的值列表或 IN 表达式中的列表 (ItemsList 的实现类)。- visit(ExpressionList expressionList) (用于 VALUES (...) 或 IN (...))
- visit(SubSelect subSelect) (用于 INSERT INTO ... SELECT ... 或 IN (SELECT ...) )
- visit(MultiExpressionList multiExprList) (用于多行 VALUES 的旧版处理)
- visit(RowConstructor rowConstructor) (JSqlParser 4.7+ 用于 VALUES (r1c1, r1c2), (r2c1, r2c2))
- 其他辅助 Visitor 接口:
- SelectBodyVisitor (与 SelectVisitor 类似,但可能是旧版或特定用途)
- OrderByVisitor / OrderByVisitorAdapter
- GroupByVisitor / GroupByVisitorAdapter
- PivotVisitor / PivotVisitorAdapter (用于 PIVOT 子句)
如何触发 Visitor 的访问:
每个 JSqlParser AST 节点(如 Statement, PlainSelect, Column, Table 等)都实现了 accept(Visitor visitor) 方法。当你调用一个节点的 accept() 方法并传入你的 Visitor 实例时,遍历就开始了。
// 假设 statement 是一个已解析的 Statement 对象
// tablesNamesCollector 是一个自定义的 Visitor,用于收集表名
TablesNamesCollector tablesNamesCollector = new TablesNamesCollector();
statement.accept(tablesNamesCollector); // 开始遍历整个 Statement AST
// 遍历完成后,可以从 tablesNamesCollector 中获取结果
List<String> tableNames = tablesNamesCollector.getTableNames();
在 accept() 方法内部,该节点会调用 Visitor 对应其自身类型的 visit() 方法。例如,如果 statement 是一个 Select 对象,statement.accept(tablesNamesCollector) 内部会执行类似 tablesNamesCollector.visit(this) 的调用,其中 this 指向该 Select 对象。
3. 实现自定义 Visitor:遍历 AST
遍历 AST 的主要目的是收集信息或对节点执行某些只读操作。
步骤:
- 创建一个类并继承相应的 VisitorAdapter (例如 StatementVisitorAdapter, ExpressionVisitorAdapter)。
- 覆盖你感兴趣的节点的 visit() 方法。
- 在 visit() 方法中实现你的逻辑。 例如,将收集到的信息存储在 Visitor 类的成员变量中。
- 确保递归访问子节点(如果需要)。 这是关键!如果一个节点包含其他也需要被访问的子节点,你需要在当前节点的 visit() 方法中显式调用子节点的 accept(this) 方法,将当前 Visitor 传递下去。
示例1:收集 SQL 语句中所有用到的表名
我们需要访问 FROM 子句中的表 (Table) 和 JOIN 子句中的表。
import com.github.jsqlparser.schema.Table;
import com.github.jsqlparser.statement.select.*;
import com.github.jsqlparser.statement.Statement;
import com.github.jsqlparser.statement.insert.Insert;
import com.github.jsqlparser.statement.update.Update;
import com.github.jsqlparser.statement.delete.Delete;
import com.github.jsqlparser.util.deparser.StatementDeParser; // For simple toString
import com.github.jsqlparser.parser.CCJSqlParserUtil;
import com.github.jsqlparser.JSQLParserException;
import java.util.ArrayList;
java
import java.util.HashSet;
import java.util.List;
import java.util.Set;
class TablesNamesCollector extends SelectVisitorAdapter implements FromItemVisitor {
private Set<String> tableNames = new HashSet<>();
public Set<String> getTableNames() {
return tableNames;
}
// --- FromItemVisitor Implementation ---
@Override
public void visit(Table tableName) { // 当访问到一个 Table 对象时被调用
tableNames.add(tableName.getFullyQualifiedName());
}
@Override
public void visit(SubSelect subSelect) { // 如果 FROM item 是子查询
// 需要递归访问子查询的 SelectBody
if (subSelect.getSelectBody() != null) {
subSelect.getSelectBody().accept(this); // `this` 就是当前的 TablesNamesCollector
}
}
@Override
public void visit(SubJoin subjoin) { // 如果 FROM item 是一个 SubJoin
if (subjoin.getLeft() != null) {
subjoin.getLeft().accept(this);
}
if (subjoin.getJoinList() != null) {
for (Join join : subjoin.getJoinList()) {
if (join.getRightItem() != null) {
join.getRightItem().accept(this);
}
}
}
}
@Override
public void visit(LateralSubSelect lateralSubSelect) {
if (lateralSubSelect.getSubSelect() != null && lateralSubSelect.getSubSelect().getSelectBody() != null) {
lateralSubSelect.getSubSelect().getSelectBody().accept(this);
}
}
// --- SelectVisitorAdapter Overrides ---
@Override
public void visit(PlainSelect plainSelect) {
// 访问 FROM item
if (plainSelect.getFromItem() != null) {
plainSelect.getFromItem().accept(this); // `this` is TablesNamesCollector which implements FromItemVisitor
}
// 访问 JOINs
if (plainSelect.getJoins() != null) {
for (Join join : plainSelect.getJoins()) {
if (join.getRightItem() != null) {
join.getRightItem().accept(this); // `this` is TablesNamesCollector
}
}
}
// 如果 WHERE 条件中可能包含子查询,也需要访问
if (plainSelect.getWhere() != null) {
// 为了访问表达式中的子查询,我们需要一个 ExpressionVisitor
// 这里简化处理,实际应用中可能需要一个组合的 Visitor 或在 ExpressionVisitor 中处理
// plainSelect.getWhere().accept(anExpressionVisitor);
}
// 同样,SelectItems, GroupBy, Having, OrderBy 中的表达式也可能包含子查询
}
@Override
public void visit(SetOperationList setOpList) { // 处理 UNION, INTERSECT 等
if (setOpList.getSelects() != null) {
for (SelectBody selectBody : setOpList.getSelects()) {
selectBody.accept(this);
}
}
}
// 如果还需要从 INSERT, UPDATE, DELETE 中提取表名,
// 则需要让 TablesNamesCollector 实现 StatementVisitor
// 并在 visit(Insert insert), visit(Update update), visit(Delete delete) 中提取表名
// 例如:
// public void visit(Insert insert) {
// if (insert.getTable() != null) {
// tableNames.add(insert.getTable().getFullyQualifiedName());
// }
// if (insert.getSelect() != null) { // For INSERT INTO ... SELECT
// insert.getSelect().getSelectBody().accept(this);
// }
// }
}java
public class VisitorTraversalDemo {
public static void main(String[] args) throws JSQLParserException {
String sql = "SELECT e.name, d.dept_name " +
"FROM employees e " +
"JOIN departments d ON e.dept_id = d.id " +
"LEFT JOIN (SELECT manager_id, COUNT(*) AS managed_count FROM employees GROUP BY manager_id) AS m_counts ON e.emp_id = m_counts.manager_id " +
"WHERE e.salary > (SELECT AVG(salary) FROM employees WHERE dept_id = e.dept_id) " +
"UNION " +
"SELECT 'N/A', dept_name FROM departments WHERE location = 'Remote'";
Statement statement = CCJSqlParserUtil.parse(sql);
TablesNamesCollector collector = new TablesNamesCollector();
// 如果只想处理 SELECT 语句的表名
if (statement instanceof Select) {
((Select) statement).getSelectBody().accept(collector);
}
// 如果想处理所有类型的语句 (需要 TablesNamesCollector 实现 StatementVisitor)
// statement.accept(collector);
System.out.println("Collected Table Names: " + collector.getTableNames());
// 输出可能包含: [employees, departments] (子查询中的 employees 也会被收集)
// 注意:这个简单的 collector 没有处理 WHERE 子查询中的表名。
// 要处理表达式中的子查询,通常需要一个 ExpressionVisitor。
}
}要点:
- TablesNamesCollector 同时实现了 SelectVisitor (通过继承 SelectVisitorAdapter) 和 FromItemVisitor。
- 在 visit(PlainSelect plainSelect) 中,我们调用 plainSelect.getFromItem().accept(this) 和 join.getRightItem().accept(this)。由于 this (即 TablesNamesCollector 实例) 实现了 FromItemVisitor,这将触发 FromItemVisitor 中相应的 visit(Table table) 或 visit(SubSelect subSelect) 方法。
- 对于 SubSelect,我们递归调用 subSelect.getSelectBody().accept(this) 来处理子查询。
- 局限性: 上述示例没有处理表达式(如 WHERE 条件、SELECT 列中的子查询)中可能出现的表名。要做到这一点,你需要一个 ExpressionVisitor,并在其中处理 SubSelect 表达式。
示例2:收集所有列引用 (需要 ExpressionVisitor)
java
import com.github.jsqlparser.expression.ExpressionVisitorAdapter;
import com.github.jsqlparser.schema.Column;
// ... other imports from previous example
class ColumnNamesCollector extends ExpressionVisitorAdapter {
private Set<String> columnNames = new HashSet<>();
public Set<String> getColumnNames() {
return columnNames;
}
@Override
public void visit(Column column) { // 当访问到一个 Column 对象时被调用
columnNames.add(column.getFullyQualifiedName());
}
// 为了遍历整个语句,通常将 ExpressionVisitor 与 StatementVisitor/SelectVisitor 结合使用
// 例如,在 SelectVisitorAdapter 的 visit(PlainSelect) 中:
// if (plainSelect.getWhere() != null) {
// plainSelect.getWhere().accept(this); // `this` 也是 ColumnNamesCollector
// }
// for (SelectItem item : plainSelect.getSelectItems()) {
// item.accept(this); // SelectItem 也需要一个 Visitor 或直接访问其 Expression
// }
}在主程序中,你需要确保 ColumnNamesCollector 被传递到所有包含表达式的地方。这通常意味着你的主 Visitor (如 SelectVisitor) 需要持有 ExpressionVisitor 的实例,或者让主 Visitor 同时实现 ExpressionVisitor。
简化演示 (实际应用需要更完整的遍历逻辑):
java
public static void main(String[] args) throws JSQLParserException {
String sql = "SELECT name, age FROM users WHERE id = 1 AND status = 'active'";
Select select = (Select) CCJSqlParserUtil.parse(sql);
PlainSelect plainSelect = (PlainSelect) select.getSelectBody();
ColumnNamesCollector columnCollector = new ColumnNamesCollector();
// 访问 WHERE 条件中的列
if (plainSelect.getWhere() != null) {
plainSelect.getWhere().accept(columnCollector);
}
// 访问 SELECT items 中的列
for (SelectItem item : plainSelect.getSelectItems()) {
if (item instanceof SelectExpressionItem) {
((SelectExpressionItem) item).getExpression().accept(columnCollector);
}
}
System.out.println("Collected Column Names: " + columnCollector.getColumnNames());
}组合 Visitor:
通常,一个复杂的操作可能需要访问 AST 的多个不同部分(语句、表达式、From项等)。你有几种方式来组织:
- 一个 Visitor 实现多个 Visitor 接口: 如 TablesNamesCollector 实现了 SelectVisitor 和 FromItemVisitor。
- 在一个 Visitor 内部实例化并使用另一个 Visitor: 例如,你的 SelectVisitor 在 visit(PlainSelect) 时,可以创建一个 ExpressionVisitor 的实例来专门处理 WHERE 条件中的表达式。
- Visitor 链: 第一个 Visitor 处理一部分,然后将其结果或修改后的 AST 传递给下一个 Visitor。
4. 实现自定义 Visitor:修改 AST
使用 Visitor 修改 AST 比遍历更复杂,因为你需要小心处理对象的引用和可变性。JSqlParser 的 AST 节点通常是可变的。
修改步骤:
- 与遍历类似,创建 Visitor 并覆盖 visit() 方法。
- 在 visit() 方法中,直接修改 AST 节点的属性。 例如,table.setName("NEW_" + table.getName())。
- 注意副作用: 修改是直接在原始 AST 对象上进行的。
- 替换节点: 有时你可能需要用一个全新的节点替换掉 AST 中的某个现有节点。这比较棘手,因为 JSqlParser 的 AST 节点通常没有提供 setParent() 或类似的方法来轻易替换自己。你通常需要在父节点的 visit() 方法中,当子节点被访问和修改(或创建新节点)后,用新的子节点去更新父节点对该子节点的引用(例如,调用父节点的 setWhere(newWhereExpression))。
示例:给所有 SELECT 语句的 WHERE 条件中自动添加一个过滤条件
假设我们要为所有 SELECT 语句添加 AND tenant_id = 'XYZ'。
import com.github.jsqlparser.expression.Expression;
import com.github.jsqlparser.expression.LongValue;
import com.github.jsqlparser.expression.Parenthesis;
import com.github.jsqlparser.expression.StringValue;
import com.github.jsqlparser.expression.operators.conditional.AndExpression;
import com.github.jsqlparser.expression.operators.relational.EqualsTo;
import com.github.jsqlparser.schema.Column;
import com.github.jsqlparser.statement.select.PlainSelect;
import com.github.jsqlparser.statement.select.Select;
import com.github.jsqlparser.statement.select.SelectVisitorAdapter;
import com.github.jsqlparser.statement.select.SetOperationList;
import com.github.jsqlparser.util.deparser.StatementDeParser;
// ... other imports
class AddTenantFilterVisitor extends SelectVisitorAdapter {
private String tenantId;
private String tenantColumnName;
public AddTenantFilterVisitor(String tenantColumnName, String tenantId) {
this.tenantColumnName \= tenantColumnName;
this.tenantId \= tenantId;
}
@Override
public void visit(PlainSelect plainSelect) {
Expression existingWhere \= plainSelect.getWhere();
// 创建新的条件: tenant\_column \= 'tenant\_value'
EqualsTo tenantCondition \= new EqualsTo();
tenantCondition.setLeftExpression(new Column(tenantColumnName)); // tenant\_id
tenantCondition.setRightExpression(new StringValue(tenantId)); // 'XYZ' (如果是数字用 LongValue)
if (existingWhere \== null) {
// 如果没有 WHERE 条件,直接设置为新条件
plainSelect.setWhere(tenantCondition);
} else {
// 如果已有 WHERE 条件,用 AND 连接
// 为了确保优先级正确,最好将原有条件用括号括起来 (如果它不是一个简单的 AndExpression)
Expression newWhere;
if (existingWhere instanceof AndExpression || existingWhere instanceof Parenthesis) {
newWhere \= new AndExpression(existingWhere, tenantCondition);
} else {
newWhere \= new AndExpression(new Parenthesis(existingWhere), tenantCondition);
}
plainSelect.setWhere(newWhere);
}
// 如果 PlainSelect 包含子查询 (例如在 FROM, WHERE, SELECT list 中),
// 你需要确保这些子查询的 SelectBody 也被这个 Visitor 访问。
// 这通常意味着在访问这些子查询的表达式时,也调用 accept(this)。
// 例如,如果 FromItem 是 SubSelect:
// if (plainSelect.getFromItem() instanceof SubSelect) {
// ((SubSelect) plainSelect.getFromItem()).getSelectBody().accept(this);
// }
// 同样,也需要处理 WHERE 条件中的子查询 (通过 ExpressionVisitor)
// 和 SELECT items 中的子查询。
}
@Override
public void visit(SetOperationList setOpList) {
// 对 UNION/INTERSECT 等操作的每个 SelectBody 都应用此逻辑
if (setOpList.getSelects() \!= null) {
for (SelectBody selectBody : setOpList.getSelects()) {
selectBody.accept(this);
}
}
}
}
public class VisitorModificationDemo {
public static void main(String[] args) throws JSQLParserException {
String sql1 = "SELECT name, salary FROM employees WHERE department = 'Sales'";
String sql2 = "SELECT product_name FROM products";
String sql3 = "SELECT c.name, o.order_date FROM customers c JOIN orders o ON c.id = o.customer_id WHERE o.amount > 1000";
String sqlUnion = "SELECT name FROM tableA WHERE id > 10 UNION SELECT name FROM tableB WHERE category = 'X'";
AddTenantFilterVisitor tenantVisitor \= new AddTenantFilterVisitor("tenant\_id", "XYZ\_TENANT");
Statement stmt1 \= CCJSqlParserUtil.parse(sql1);
if (stmt1 instanceof Select) {
((Select) stmt1).getSelectBody().accept(tenantVisitor);
}
System.out.println("Modified SQL 1: " \+ stmt1);
// Expected: SELECT name, salary FROM employees WHERE (department \= 'Sales') AND tenant\_id \= 'XYZ\_TENANT'
Statement stmt2 \= CCJSqlParserUtil.parse(sql2);
if (stmt2 instanceof Select) {
((Select) stmt2).getSelectBody().accept(tenantVisitor);
}
System.out.println("Modified SQL 2: " \+ stmt2);
// Expected: SELECT product\_name FROM products WHERE tenant\_id \= 'XYZ\_TENANT'
Statement stmt3 \= CCJSqlParserUtil.parse(sql3);
if (stmt3 instanceof Select) {
((Select) stmt3).getSelectBody().accept(tenantVisitor);
}
System.out.println("Modified SQL 3: " \+ stmt3);
// Expected: SELECT c.name, o.order\_date FROM customers c JOIN orders o ON c.id \= o.customer\_id WHERE (o.amount \> 1000\) AND tenant\_id \= 'XYZ\_TENANT'
Statement stmtUnion \= CCJSqlParserUtil.parse(sqlUnion);
if (stmtUnion instanceof Select) {
((Select) stmtUnion).getSelectBody().accept(tenantVisitor);
}
System.out.println("Modified SQL Union: " \+ stmtUnion);
// Expected: (SELECT name FROM tableA WHERE (id \> 10\) AND tenant\_id \= 'XYZ\_TENANT') UNION (SELECT name FROM tableB WHERE (category \= 'X') AND tenant\_id \= 'XYZ\_TENANT')
}
}
注意事项:
- 递归修改: 上述 AddTenantFilterVisitor 示例仅处理了顶层的 PlainSelect 和 SetOperationList。在一个真实的、复杂的 SQL 中,子查询可能出现在 FROM 子句、WHERE 条件、SELECT 列、JOIN ON 条件等多个地方。你需要确保你的 Visitor 能够递归地访问并修改这些嵌套的 SelectBody。这通常涉及到让你的 Visitor 也实现 ExpressionVisitor 和 FromItemVisitor,并在访问 SubSelect (作为表达式或 FromItem) 时,对其 SelectBody 调用 accept(this)。
- 深拷贝: 如果你不想修改原始 AST,而想创建一个修改后的副本,你需要自己实现深拷贝逻辑,因为 JSqlParser 本身不直接提供完整的 AST 深拷贝功能。这通常很复杂。
- 引用完整性: 当替换节点时,要确保所有指向旧节点的引用都被更新为指向新节点。
5. JSqlParser 提供的 DeParser (反向解析)
修改完 AST 后,我们通常需要将其转换回 SQL 字符串。JSqlParser 提供了 DeParser (反向解析器) 来实现这个功能。
核心的 DeParser 类有:
- com.github.jsqlparser.util.deparser.StatementDeParser
- com.github.jsqlparser.util.deparser.SelectDeParser
- com.github.jsqlparser.util.deparser.ExpressionDeParser
这些 DeParser 本身也是 Visitor!它们通过访问 AST 节点,并将每个节点转换成其对应的 SQL 字符串片段,然后将这些片段拼接起来。
基本用法:
当你调用一个 AST 节点的 toString() 方法时 (例如 statement.toString()),JSqlParser 内部实际上就是在使用一个默认的 DeParser 实例来生成 SQL 字符串。
Statement modifiedStatement = ... // 经过 Visitor 修改后的 AST
String newSql = modifiedStatement.toString();
System.out.println(newSql);
自定义 DeParser 的行为:
DeParser 类通常接受一个 StringBuilder 作为构造参数(或通过 setBuffer() 方法设置),用于构建输出的 SQL 字符串。它们还可能提供一些配置选项或方法,允许你定制输出 SQL 的格式(例如,关键字大小写、缩进等),但这方面的功能相对有限。
如果你需要高度定制化的 SQL 输出格式,你可能需要:
- 继承 JSqlParser 提供的 DeParser 类,并覆盖某些 visit() 方法来改变特定节点的输出行为。
- 完全实现自己的 DeParser (实现相应的 Visitor 接口),但这工作量较大。
DeParser 如何与修改 Visitor 配合:
使用你的自定义修改 Visitor (如 AddTenantFilterVisitor) 访问并修改 AST。
Statement stmt = CCJSqlParserUtil.parse(originalSql);
AddTenantFilterVisitor modifier = new AddTenantFilterVisitor("tenant_id", "ABC");
stmt.accept(modifier); // 或者 selectBody.accept(modifier)直接调用修改后 AST 节点的 toString() 方法即可得到新的 SQL 字符串。
String modifiedSql = stmt.toString();
或者,如果你想更精细地控制 DeParsing 过程:
Statement stmt = CCJSqlParserUtil.parse(originalSql);
// ... (修改 stmt 的逻辑) ...
StringBuilder buffer = new StringBuilder();
// StatementDeParser 需要一个 ExpressionDeParser 和一个 SelectDeParser 作为参数
// 通常可以创建默认的实例
ExpressionDeParser expressionDeParser = new ExpressionDeParser();
SelectDeParser selectDeParser = new SelectDeParser(expressionDeParser, buffer);
expressionDeParser.setSelectVisitor(selectDeParser); // 解决循环依赖
expressionDeParser.setBuffer(buffer);
StatementDeParser deParser = new StatementDeParser(expressionDeParser, selectDeParser, buffer);
stmt.accept(deParser);
String newSqlString = buffer.toString();
对于大多数场景,直接调用 statement.toString() 已经足够。
动手实验 5:
- 遍历任务 - 统计函数调用:
- 编写一个 Visitor (例如,继承 ExpressionVisitorAdapter),用于收集 SQL 语句中所有被调用的函数名称及其参数数量。
- 例如,对于 SELECT COUNT(*), SUBSTRING(name, 1, 3), NOW() FROM users,应能收集到:
- COUNT (1 个参数,* 算一个)
- SUBSTRING (3 个参数)
- NOW (0 个参数)
- 你需要一个主 Visitor (如 SelectVisitorAdapter) 来遍历 SELECT 语句的各个部分 (select items, where, having 等),并在遇到表达式时,调用你的函数收集 Visitor。
- 修改任务 - 表名统一添加前缀:
- 编写一个 Visitor (例如,继承 StatementVisitorAdapter 并实现 FromItemVisitor),将 SQL 语句中所有引用的表名(包括 FROM 子句和 JOIN 子句中的表)统一添加一个前缀,例如 DEV_。
- 例如,SELECT * FROM users JOIN orders ON users.id = orders.user_id 应修改为 SELECT * FROM DEV_users JOIN DEV_orders ON DEV_users.id = DEV_orders.user_id。
- 注意: 你还需要修改 Column 对象中的表名引用 (例如 users.id -> DEV_users.id)。这需要你的 Visitor 也处理 Column 表达式。
- 使用 DeParser 将修改后的 AST 转换回 SQL 字符串并打印。
- (挑战) 修改任务 - 参数化敏感字面量:
- 编写一个 Visitor,查找 WHERE 条件中的字符串字面量或数字字面量(例如,name = 'Alice' 或 age = 30),并将它们替换为 JDBC 参数占位符 ?。
- 收集被替换掉的原始值,按顺序存储。
- 例如,SELECT * FROM users WHERE name = 'Bob' AND city = 'London'
修改为 SELECT * FROM users WHERE name = ? AND city = ?
收集到的值为 ['Bob', 'London']。 - 这需要一个 ExpressionVisitor,并且在修改时要小心处理父节点对子节点的引用。
模块五小结:
在本模块中,我们深入探讨了 Visitor 设计模式及其在 JSqlParser 中的应用:
- 理解了 Visitor 模式如何将操作与对象结构分离,方便对 AST 进行扩展操作。
- 熟悉了 JSqlParser 提供的核心 Visitor 接口 (StatementVisitor, SelectVisitor, ExpressionVisitor, FromItemVisitor 等) 及其适配器类。
- 掌握了如何通过继承 VisitorAdapter 并覆盖 visit() 方法来实现自定义的 AST 遍历逻辑,以收集信息。
- 学习了如何通过在 visit() 方法中直接修改 AST 节点属性来实现对 SQL 结构的修改,并了解了其中的注意事项(递归修改、节点替换的复杂性)。
- 了解了 JSqlParser 的 DeParser 如何将 AST 反向解析为 SQL 字符串,以及如何与修改 Visitor 配合使用。
Visitor 模式是使用 JSqlParser 进行高级 SQL 分析和操作的基石。熟练掌握它将使你能够处理非常复杂的 SQL 处理任务。