Phân tích cú pháp Subquery trong Druid SQL Parser

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.MYSQL hoặ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: SQLQueryExpr dùng cho scalar subquery (trong SELECT hoặc WHERE so sánh), trong khi SQLSubqueryTableSource dù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 trong SQLBinaryOpExpr. 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.

Thẻ: Druid sql-parser Java AST subquery

Đăng vào ngày 26 tháng 6 lúc 06:39