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ính | Kiểu dữ liệu | Mô tả |
|---|---|---|
withClause | List<SQLWithClause> | Danh sách các biểu thức CTE (chỉ hỗ trợ ở MySQL 8.0+, Oracle) |
query | SQLSelectQuery | Giao diện chung cho phần logic truy vấn |
Các phương thức tiện ích:
getQuery(): Trả về đối tượngSQLSelectQueryđ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ớiSELECT,FROM,WHERE, v.v.SQLUnionQuery: Xử lý các truy vấn kết hợp nhưUNION ALL,EXCEPT, hoặcINTERSECT.
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ính | Kiểu | Mô tả |
|---|---|---|
selectList | List<SQLSelectItem> | Danh sách cột hoặc biểu thức trong mệnh đề SELECT |
from | SQLTableSource | Nguồn dữ liệu: bảng, alias, subquery hoặc join |
where | SQLExpr | Biểu thức điều kiện trong WHERE |
groupBy | SQLGroupBy | Mệnh đề GROUP BY |
having | SQLExpr | Điều kiện lọc sau nhóm (HAVING) |
orderBy | SQLOrderBy | Thứ tự sắp xếp kết quả |
limit | SQLLimit | Giới hạn số bản ghi trả về |
distinct | boolean | Có á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ính | Kiểu | Mô tả |
|---|---|---|
left | SQLSelectQuery | Truy vấn bên trái của phép toán |
right | SQLSelectQuery | Truy vấn bên phải |
unionType | SQLUnionQuery.UnionType | Loạ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 đơnblock.getSelectList()→ danh sách cộtblock.getFrom()→ nguồn dữ liệublock.getWhere()→ điều kiện hiện tạiblock.addWhere(...)→ thêm điều kiện mớiblock.getOrderBy()→ thứ tự sắp xếpblock.getLimit()→ cấu hình phân trangSQLUtils.toSQLString(...)→ chuyển AST thành chuỗi SQL