Phân tích Cấu trúc Truy vấn SQL trong Druid: Lớp SQLSelect và Các Thành Phần Liên quan

Trong thư viện Druid, việc phân tích cú pháp SQL phụ thuộc vào một hệ thống lớp đối tượng được thiết kế theo mô hình cây cú pháp trừu tượng (AST). Trong đó, SQLSelect là thành phần trung tâm nhất, đại diện cho toàn bộ nội dung logic của một truy vấn SELECT. Bài viết này trình bày lại kiến trúc và cách sử dụng SQLSelect dưới góc nhìn kỹ thuật hiện đại — tập trung vào tính rõ ràng, khả năng mở rộng và ứng dụng thực tế.

Cấu trúc phân cấp chính

Mỗi câu lệnh SELECT sau khi được phân tích sẽ tạo ra một chuỗi các đối tượng có quan hệ cha-con chặt chẽ:

SQLSelectStatement  // Đại diện cho toàn bộ câu lệnh SQL
└── SQLSelect         // Đóng vai trò "container" cho phần thân truy vấn
    ├── withClause    // Danh sách các CTE (WITH clause), nếu có
    └── query         // Thực thể chứa logic truy vấn thực sự
        ├── SQLSelectQueryBlock   // Trường hợp phổ biến: SELECT ... FROM ... WHERE ...
        └── SQLUnionQuery         // Trường hợp phức tạp: UNION/EXCEPT/INTERSECT

Các lớp cốt lõi và chức năng cụ thể

1. SQLSelect

Là lớp gốc mô tả một truy vấn hoàn chỉnh. Nó không phải là câu lệnh (statement), mà là phần thân (body) của truy vấn — nơi lưu trữ cả khối WITH và phần truy vấn chính.

Thuộc tínhKiểu dữ liệuMô tả
withClauseList<SQLWithClause>Danh sách các biểu thức CTE (chỉ hỗ trợ ở MySQL 8.0+, Oracle)
querySQLSelectQueryGiao diện chung cho phần logic truy vấn

Các phương thức tiện ích:

  • getQuery(): Trả về đối tượng SQLSelectQuery đang được sử dụng.
  • getQueryBlock(): Nếu truy vấn là dạng đơn (không phải UNION), trả về SQLSelectQueryBlock; ngược lại trả về null.
  • setQuery(...): Thay thế toàn bộ phần thân truy vấn bằng một đối tượng mới.

2. SQLSelectQuery và hai triển khai

Đây là giao diện trừu tượng định nghĩa hành vi chung của mọi loại truy vấn con. Hai triển khai chính gồm:

  • SQLSelectQueryBlock: Dùng cho hầu hết các trường hợp — truy vấn đơn giản với SELECT, FROM, WHERE, v.v.
  • SQLUnionQuery: Xử lý các truy vấn kết hợp như UNION ALL, EXCEPT, hoặc INTERSECT.

3. SQLSelectQueryBlock

Là lớp được sử dụng nhiều nhất trong thực tiễn phát triển. Mỗi thuộc tính tương ứng với một mệnh đề SQL chuẩn:

Tên thuộc tínhKiểuMô tả
selectListList<SQLSelectItem>Danh sách cột hoặc biểu thức trong mệnh đề SELECT
fromSQLTableSourceNguồn dữ liệu: bảng, alias, subquery hoặc join
whereSQLExprBiểu thức điều kiện trong WHERE
groupBySQLGroupByMệnh đề GROUP BY
havingSQLExprĐiều kiện lọc sau nhóm (HAVING)
orderBySQLOrderByThứ tự sắp xếp kết quả
limitSQLLimitGiới hạn số bản ghi trả về
distinctbooleanCó áp dụng DISTINCT hay không

4. SQLUnionQuery

Mô hình hóa các phép toán tập hợp giữa hai truy vấn con:

Thuộc tínhKiểuMô tả
leftSQLSelectQueryTruy vấn bên trái của phép toán
rightSQLSelectQueryTruy vấn bên phải
unionTypeSQLUnionQuery.UnionTypeLoại phép toán: UNION, UNION_ALL, EXCEPT, INTERSECT

Ví dụ minh họa thực tế

Phụ thuộc Maven

<dependency>
  <groupId>com.alibaba</groupId>
  <artifactId>druid</artifactId>
  <version>1.2.21</version>
</dependency>

Ví dụ 1: Phân tích truy vấn đơn giản

import com.alibaba.druid.sql.SQLUtils;
import com.alibaba.druid.sql.ast.SQLSelect;
import com.alibaba.druid.sql.ast.statement.SQLSelectQueryBlock;
import com.alibaba.druid.sql.ast.statement.SQLSelectStatement;
import com.alibaba.druid.util.JdbcConstants;

public class QueryAnalyzer {
  public static void main(String[] args) {
    String rawSql = "SELECT id, full_name, score FROM students WHERE score >= 85 ORDER BY score DESC LIMIT 20";

    SQLSelectStatement stmt = (SQLSelectStatement) SQLUtils.parseSingleStatement(
        rawSql, JdbcConstants.MYSQL);

    SQLSelect selectNode = stmt.getSelect();
    SQLSelectQueryBlock block = selectNode.getQueryBlock();

    System.out.println("Các cột được chọn: " + block.getSelectList());
    System.out.println("Nguồn dữ liệu: " + block.getFrom());
    System.out.println("Điều kiện lọc: " + block.getWhere());
    System.out.println("Sắp xếp: " + block.getOrderBy());
  }
}

Ví dụ 2: Nhận diện và xử lý truy vấn UNION

String unionSql = "SELECT id FROM active_users UNION SELECT id FROM archived_users";
SQLSelectStatement unionStmt = (SQLSelectStatement) SQLUtils.parseSingleStatement(
    unionSql, JdbcConstants.MYSQL);

SQLSelect unionSelect = unionStmt.getSelect();
if (unionSelect.getQuery() instanceof SQLUnionQuery) {
  SQLUnionQuery uq = (SQLUnionQuery) unionSelect.getQuery();
  System.out.println("Loại phép toán: " + uq.getUnionType());
  System.out.println("Truy vấn trái: " + uq.getLeft().toString());
  System.out.println("Truy vấn phải: " + uq.getRight().toString());
}

Ví dụ 3: Tiêm điều kiện bảo mật (multi-tenancy)

String baseSql = "SELECT * FROM orders";
SQLSelectStatement baseStmt = (SQLSelectStatement) SQLUtils.parseSingleStatement(
    baseSql, JdbcConstants.MYSQL);

SQLSelectQueryBlock targetBlock = baseStmt.getSelect().getQueryBlock();
targetBlock.addWhere(SQLUtils.toSQLExpr("tenant_id = 'tenant_abc'"));

System.out.println("SQL đã chỉnh sửa: " + SQLUtils.toSQLString(baseStmt));
// Kết quả: SELECT * FROM orders WHERE tenant_id = 'tenant_abc'

Ví dụ 4: Liệt kê tất cả tên bảng bằng Visitor Pattern

baseStmt.accept(new SQLASTVisitorAdapter() {
  @Override
  public boolean visit(SQLExprTableSource tableSource) {
    System.out.println("Tên bảng được tham chiếu: " + tableSource.getExpr().toString());
    return true;
  }
});

Tóm tắt nhanh các thao tác thường dùng

  • select.getQueryBlock() → lấy khối truy vấn đơn
  • block.getSelectList() → danh sách cột
  • block.getFrom() → nguồn dữ liệu
  • block.getWhere() → điều kiện hiện tại
  • block.addWhere(...) → thêm điều kiện mới
  • block.getOrderBy() → thứ tự sắp xếp
  • block.getLimit() → cấu hình phân trang
  • SQLUtils.toSQLString(...) → chuyển AST thành chuỗi SQL

Thẻ: Druid sql-parser AST Java sql-query-analysis

Đăng vào ngày 30 tháng 6 lúc 07:30