1. Phân loại các节点 Subquery trong AST của Druid
Trong Druid SQL Parser, mỗi vị trí xuất hiện của subquery sẽ được ánh xạ đến một lớp Java cụ thể trong cây cú pháp trừu tượng (AST). Việc nắm vững mối quan hệ này là chìa khóa để phân tích và biến đổi SQL chính xác.
| Vị trí xuất hiện | Cú pháp SQL mẫu | Lớp Druid tương ứng | Lớp cha | Chức năng chính |
|---|---|---|---|---|
| Mệnh đề WHERE | id IN (SELECT user_id FROM order) | SQLInSubQueryExpr | SQLExpr | Kiểm tra giá trị trường có nằm trong tập kết quả không |
| Mệnh đề WHERE | EXISTS (SELECT 1 FROM order WHERE...) | SQLExistsExpr | SQLExpr | Kiểm tra sự tồn tại của dữ liệu trong subquery |
| Mệnh đề WHERE | age > ANY (SELECT age FROM vip) | SQLAnyExpr | SQLExpr | So sánh với bất kỳ giá trị nào trong tập kết quả |
| Mệnh đề WHERE | age > ALL (SELECT age FROM vip) | SQLAllExpr | SQLExpr | So sánh với tất cả giá trị trong tập kết quả |
| Mệnh đề WHERE | age = (SELECT MAX(age) FROM user) | SQLBinaryOpExpr (chứa SQLQueryExpr) | SQLExpr | Sử dụng toán tử so sánh trực tiếp với kết quả subquery |
| Mệnh đề FROM | FROM (SELECT id FROM user) AS t | SQLSubqueryTableSource | SQLTableSource | Subquery đóng vai trò như một bảng tạm (derived table) |
| Danh sách SELECT | SELECT (SELECT name FROM...) AS col | SQLQueryExpr | SQLExpr | Scalar subquery, trả về một giá trị đơn làm cột kết quả |
2. Cấu hình môi trường phát triển
Để thực hiện phân tích cú pháp, cần đảm bảo dự án đã tích hợp thư viện Druid phiên bản ổn định.
2.1. Maven Dependency
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.20</version>
</dependency>
2.2. Các class cần import
Đống bộ các package cần thiết cho việc xử lý AST và tiện ích SQL:
import com.alibaba.druid.sql.SQLUtils;
import com.alibaba.druid.sql.ast.SQLExpr;
import com.alibaba.druid.sql.ast.SQLSelect;
import com.alibaba.druid.sql.ast.expr.*;
import com.alibaba.druid.sql.ast.statement.*;
import com.alibaba.druid.util.JdbcConstants;
import java.util.List;
3. Thực hành phân tích theo từng trường hợp
Dưới đây là các ví dụ cụ thể cho từng vị trí xuất hiện của subquery, kèm theo mã nguồn Java để trích xuất và biến đổi.
3.1. Subquery trong mệnh đề WHERE
Đây là trường hợp phổ biến nhất. Các biểu thức điều kiện đều kế thừa từ SQLExpr. Quy trình chung là lấy biểu thức WHERE sau đó kiểm tra kiểu dữ liệu cụ thể.
3.1.1. Xử lý IN Subquery
Kiểm tra xem một trường có nằm trong tập hợp kết quả trả về hay không.
public class InSubqueryInspector {
public static void main(String[] args) {
String queryStatement = "SELECT id, name FROM user WHERE id IN (SELECT user_id FROM `order` WHERE status = 1)";
// Phân tích cú pháp SQL
SQLSelectStatement statement = (SQLSelectStatement) SQLUtils.parseSingleStatement(queryStatement, JdbcConstants.MYSQL);
SQLSelectQueryBlock rootBlock = statement.getSelect().getQueryBlock();
SQLExpr conditionExpr = rootBlock.getWhere();
// Kiểm tra loại biểu thức
if (conditionExpr instanceof SQLInSubQueryExpr) {
SQLInSubQueryExpr inExpr = (SQLInSubQueryExpr) conditionExpr;
SQLExpr targetColumn = inExpr.getExpr();
SQLSelect innerSelect = inExpr.getSubQuery();
SQLSelectQueryBlock innerBlock = innerSelect.getQueryBlock();
System.out.println("Cột kiểm tra: " + SQLUtils.toSQLString(targetColumn));
System.out.println("SQL con: " + SQLUtils.toSQLString(innerSelect));
System.out.println("Bảng trong SQL con: " + SQLUtils.toSQLString(innerBlock.getFrom()));
}
}
}
3.1.2. Xử lý EXISTS Subquery
Dùng để kiểm tra sự tồn tại của bản ghi, thường đi kèm với điều kiện tương quan.
public class ExistsSubqueryModifier {
public static void main(String[] args) {
String queryStatement = "SELECT id, name FROM user u WHERE EXISTS (SELECT 1 FROM `order` o WHERE o.user_id = u.id)";
SQLSelectStatement statement = (SQLSelectStatement) SQLUtils.parseSingleStatement(queryStatement, JdbcConstants.MYSQL);
SQLExpr conditionExpr = statement.getSelect().getQueryBlock().getWhere();
if (conditionExpr instanceof SQLExistsExpr) {
SQLExistsExpr existsNode = (SQLExistsExpr) conditionExpr;
SQLSelect innerSelect = existsNode.getSubQuery();
SQLSelectQueryBlock innerBlock = innerSelect.getQueryBlock();
System.out.println("Trạng thái NOT: " + existsNode.isNot());
// Thêm điều kiện mới vào subquery
SQLExpr newCondition = SQLUtils.toSQLExpr("o.create_time > '2024-01-01'", JdbcConstants.MYSQL);
innerBlock.addWhere(newCondition);
System.out.println("SQL sau khi sửa: " + SQLUtils.toSQLString(statement));
}
}
}
3.1.3. Xử lý ANY và ALL
Dùng cho các phép so sánh tập hợp.
public class AnyAllSubqueryParser {
public static void main(String[] args) {
String[] queries = {
"SELECT id FROM user WHERE age > ANY (SELECT age FROM vip)",
"SELECT id FROM user WHERE age > ALL (SELECT age FROM vip)"
};
for (String q : queries) {
analyzeAnyAll(q);
}
}
private static void analyzeAnyAll(String sql) {
SQLSelectStatement statement = (SQLSelectStatement) SQLUtils.parseSingleStatement(sql, JdbcConstants.MYSQL);
SQLExpr conditionExpr = statement.getSelect().getQueryBlock().getWhere();
if (conditionExpr instanceof SQLAnyExpr) {
System.out.println("Loại: ANY - " + SQLUtils.toSQLString(((SQLAnyExpr) conditionExpr).getSubQuery()));
} else if (conditionExpr instanceof SQLAllExpr) {
System.out.println("Loại: ALL - " + SQLUtils.toSQLString(((SQLAllExpr) conditionExpr).getSubQuery()));
}
}
}
3.1.4. Subquery với toán tử so sánh (Scalar)
Trường hợp này dễ bị bỏ qua vì nó nằm trong một biểu thức nhị phân (SQLBinaryOpExpr). Vế phải của phép so sánh chính là subquery.
public class ComparisonSubqueryHandler {
public static void main(String[] args) {
String queryStatement = "SELECT id FROM user WHERE age = (SELECT MAX(age) FROM user WHERE dept_id = 3)";
SQLSelectStatement statement = (SQLSelectStatement) SQLUtils.parseSingleStatement(queryStatement, JdbcConstants.MYSQL);
SQLExpr conditionExpr = statement.getSelect().getQueryBlock().getWhere();
if (conditionExpr instanceof SQLBinaryOpExpr) {
SQLBinaryOpExpr binaryNode = (SQLBinaryOpExpr) conditionExpr;
SQLExpr rightSide = binaryNode.getRight();
// Kiểm tra xem vế phải có phải là subquery không
if (rightSide instanceof SQLQueryExpr) {
SQLQueryExpr queryNode = (SQLQueryExpr) rightSide;
SQLSelect innerSelect = queryNode.getSubQuery();
System.out.println("Toán tử: " + binaryNode.getOperator());
System.out.println("Subquery: " + SQLUtils.toSQLString(innerSelect));
// Sửa điều kiện bên trong subquery
innerSelect.getQueryBlock().setWhere(
SQLUtils.toSQLExpr("dept_id = 4", JdbcConstants.MYSQL)
);
System.out.println("SQL mới: " + SQLUtils.toSQLString(statement));
}
}
}
}
3.2. Subquery trong mệnh đề FROM
Subquery ở đây đóng vai trò là một bảng nguồn (SQLTableSource). Bắt buộc phải có alias.
public class FromSubqueryAnalyzer {
public static void main(String[] args) {
String queryStatement = "SELECT t.id FROM (SELECT id FROM user WHERE age > 18) AS t WHERE t.id < 30";
SQLSelectStatement statement = (SQLSelectStatement) SQLUtils.parseSingleStatement(queryStatement, JdbcConstants.MYSQL);
SQLTableSource tableSource = statement.getSelect().getQueryBlock().getFrom();
if (tableSource instanceof SQLSubqueryTableSource) {
SQLSubqueryTableSource subTable = (SQLSubqueryTableSource) tableSource;
System.out.println("Alias: " + subTable.getAlias());
System.out.println("Nội dung: " + SQLUtils.toSQLString(subTable.getSelect()));
// Thêm điều kiện vào bảng tạm
subTable.getSelect().getQueryBlock().addWhere(
SQLUtils.toSQLExpr("name LIKE '%Test%'", JdbcConstants.MYSQL)
);
}
}
}
3.3. Subquery trong danh sách SELECT
Thường dùng để lấy dữ liệu liên quan mà không cần JOIN, trả về một giá trị vô hướng.
public class SelectListSubqueryReader {
public static void main(String[] args) {
String queryStatement = "SELECT id, (SELECT name FROM user WHERE id = order.user_id) AS uname FROM `order`";
SQLSelectStatement statement = (SQLSelectStatement) SQLUtils.parseSingleStatement(queryStatement, JdbcConstants.MYSQL);
List<SQLSelectItem> items = statement.getSelect().getQueryBlock().getSelectList();
for (SQLSelectItem item : items) {
SQLExpr expr = item.getExpr();
if (expr instanceof SQLQueryExpr) {
System.out.println("Cột tính toán: " + SQLUtils.toSQLString(((SQLQueryExpr) expr).getSubQuery()));
} else {
System.out.println("Cột thường: " + SQLUtils.toSQLString(expr));
}
}
}
}
4. Công cụ phân tích tổng quát (Visitor Pattern)
Để xử lý nhiều loại subquery cùng lúc hoặc các subquery lồng nhau, nên sử dụng cơ chế Visitor để duyệt toàn bộ cây AST.
import com.alibaba.druid.sql.ast.SQLObject;
import com.alibaba.druid.sql.visitor.SQLASTVisitorAdapter;
public class DruidSubqueryInspector {
public static void scanAllSubqueries(String sql) {
SQLSelectStatement statement = (SQLSelectStatement) SQLUtils.parseSingleStatement(sql, JdbcConstants.MYSQL);
statement.accept(new SQLASTVisitorAdapter() {
@Override
public boolean visit(SQLInSubQueryExpr x) {
System.out.println("Phát hiện IN Subquery: " + SQLUtils.toSQLString(x.getSubQuery()));
return true;
}
@Override
public boolean visit(SQLExistsExpr x) {
System.out.println("Phát hiện EXISTS Subquery: " + SQLUtils.toSQLString(x.getSubQuery()));
return true;
}
@Override
public boolean visit(SQLSubqueryTableSource x) {
System.out.println("Phát hiện FROM Subquery: " + SQLUtils.toSQLString(x.getSelect()));
return true;
}
@Override
public boolean visit(SQLQueryExpr x) {
System.out.println("Phát hiện Scalar Subquery: " + SQLUtils.toSQLString(x.getSubQuery()));
return true;
}
});
}
public static void main(String[] args) {
String complexSql = "SELECT id FROM (SELECT id FROM user) t WHERE id IN (SELECT id FROM order)";
scanAllSubqueries(complexSql);
}
}
5. Các lưu ý kỹ thuật quan trọng
- Chỉ định Dialect: Luôn truyền tham số
JdbcConstants.MYSQLhoặc tương đương khi parse. Nếu không, các cú pháp đặc thù có thể không được nhận diện đúng. - Phân biệt lớp:
SQLQueryExprdùng cho scalar subquery (trong SELECT hoặc WHERE so sánh), trong khiSQLSubqueryTableSourcedùng cho derived table (trong FROM). Cha của chúng khác nhau. - Toán tử so sánh: Khi subquery nằm sau dấu
=,>, nó được bọc trongSQLBinaryOpExpr. Cần lấy vế phải (getRight()) để kiểm tra. - Alias bắt buộc: Subquery trong mệnh đề FROM bắt buộc phải có alias, nếu không SQL sẽ không hợp lệ dù parser có thể không báo lỗi ngay.
- Đệ quy: Visitor pattern tự động hỗ trợ duyệt các subquery lồng nhau nhiều lớp mà không cần viết logic đệ quy thủ công.