Sử Dụng MyBatis Interceptor Để Tùy Chỉnh SQL Execution

Nguyên Lý Hoạt Động Của MyBatis Interceptor Và Các Ví Dụ Thực Tế

1. Nguyên Lý Hoạt Động

MyBatis sử dụng **Java Dynamic Proxy** và **Chain of Responsibility Pattern** để cho phép các nhà phát triển chèn logic tùy chỉnh vào các điểm quan trọng trong quá trình thực thi SQL (như tạo SQL, thiết lập tham số, xử lý kết quả). Cơ chế chính bao gồm:
  1. Tạo Đối Tượng Proxy: MyBatis tạo ra đối tượng proxy cho các giao diện cần chặn (ví dụ: `Executor`, `StatementHandler`). Nếu có nhiều interceptor, sẽ hình thành chuỗi proxy (proxy ngoài gọi proxy trong, cuối cùng gọi đến đối tượng gốc).
  2. Khai Báo Mục Tiêu Chặn: Sử dụng các chú thích `@Intercepts` và `@Signature` để xác định mục tiêu chặn:
    • `@Intercepts`: Gói một hoặc nhiều `@Signature`, biểu thị nhóm mục tiêu bị chặn.
    • `@Signature`: Định nghĩa mục tiêu chặn riêng lẻ, chứa ba thuộc tính:
      • `type`: Giao diện bị chặn (ví dụ: `Executor`, `StatementHandler`).
      • `method`: Tên phương thức bị chặn (ví dụ: `query`, `update`).
      • `args`: Mảng kiểu tham số của phương thức bị chặn (phải khớp hoàn toàn với phương thức giao diện).
  3. Quy Trình Thực Thi:
    • Khi khởi động, MyBatis quét các interceptor và tạo đối tượng proxy cho các giao diện mục tiêu.
    • Khi gọi phương thức mục tiêu, đối tượng proxy trước tiên thực hiện phương thức `intercept` (logic tùy chỉnh), sau đó gọi phương thức gốc.
    • Nếu có nhiều interceptor, tất cả logic chặn sẽ được thực hiện theo thứ tự trước khi gọi phương thức gốc.

2. Các Thành Phần Chính Và Vai Trò Tham Số

MyBatis Interceptor chủ yếu chặn bốn thành phần cốt lõi, mỗi thành phần chịu trách nhiệm về các giai đoạn khác nhau trong việc xử lý SQL.
Thành PhầnVai TròCác Phương Thức Thường Bị ChặnVai Trò Tham Số
ExecutorQuản lý toàn bộ quá trình thực thi SQL (ví dụ: truy vấn, cập nhật, kiểm soát giao dịch).`query`, `update``MappedStatement`: Chứa thông tin ánh xạ SQL; `Object`: Đối tượng tham số; `RowBounds`: Tham số phân trang.
StatementHandlerXử lý chuẩn bị câu lệnh SQL (ví dụ: tạo Statement), thiết lập tham số, ánh xạ tập kết quả.`prepare`, `update`, `query``Connection`: Kết nối cơ sở dữ liệu; `Integer`: Thời gian chờ; `Statement`: Đối tượng JDBC Statement.
ParameterHandlerXử lý thiết lập tham số SQL (ví dụ: thiết lập tham số cho PreparedStatement).`setParameters``PreparedStatement`: Câu lệnh đã biên dịch; `Object`: Đối tượng tham số.
ResultSetHandlerXử lý ánh xạ tập kết quả truy vấn (ví dụ: chuyển đổi tập kết quả thành đối tượng Java).`handleResultSets``Statement`: Câu lệnh đã thực thi; `ResultHandler`: Bộ xử lý kết quả.

3. Các Ví Dụ Sử Dụng

Ví Dụ 1: Thống Kê Thời Gian Thực Thi SQL (Chặn Executor)

@Intercepts({
    @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
    @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})
})
public class TinhThoiGianSQLInterceptor implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        long batDau = System.currentTimeMillis();
        try {
            return invocation.proceed(); // Thực thi phương thức gốc
        } finally {
            long ketThuc = System.currentTimeMillis();
            long chiPhi = ketThuc - batDau;
            MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
            String idSQL = ms.getId(); // Toàn đường dẫn phương thức Mapper
            System.out.printf("Thực thi SQL:%s, Chi phí:%d ms%n", idSQL, chiPhi);
        }
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this); // Tạo đối tượng proxy
    }

    @Override
    public void setProperties(Properties properties) {
        // Nhận tham số cấu hình (ví dụ: ngưỡng SQL chậm)
    }
}
Cấu Hình (Cấu hình bản địa của MyBatis):
<plugins>
    <plugin interceptor="com.example.TinhThoiGianSQLInterceptor">
        <property name="slowSqlThreshold" value="500" />
    </plugin>
</plugins>

Ví Dụ 2: Phân Trang Động (Chặn StatementHandler)

@Intercepts(@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class}))
public class PhanTrangInterceptor implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
        MetaObject metaObj = SystemMetaObject.forObject(statementHandler);
        BoundSql boundSql = statementHandler.getBoundSql();
        String sqlGoc = boundSql.getSql();
        Object doiTuongThamSo = boundSql.getParameterObject();

        if (doiTuongThamSo instanceof Map) {
            Map mapParam = (Map) doiTuongThamSo;
            if (mapParam.containsKey("trangHienTai") && mapParam.containsKey("kichThuocTrang")) {
                int trangHienTai = (int) mapParam.get("trangHienTai");
                int kichThuocTrang = (int) mapParam.get("kichThuocTrang");
                String sqlPhanTrang = sqlGoc + " LIMIT " + (trangHienTai - 1) * kichThuocTrang + ", " + kichThuocTrang;
                metaObj.setValue("boundSql.sql", sqlPhanTrang); // Sửa SQL
            }
        }
        return invocation.proceed();
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {
    }
}

Ví Dụ 3: Phân Tách Đọc Viết (Chặn Executor)

@Intercepts({
    @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
    @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
public class PhanTachDocVietInterceptor implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        String tenPhuongThuc = invocation.getMethod().getName();
        MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
        if (ms.getSqlCommandType().equals(SqlCommandType.SELECT)) {
            QuanLyNguonDuLieu.setLoaiNguon("slave"); // Đọc dùng từ nguồn phụ
        } else {
            QuanLyNguonDuLieu.setLoaiNguon("master"); // Ghi dùng từ nguồn chính
        }
        try {
            return invocation.proceed();
        } finally {
            QuanLyNguonDuLieu.xoaLoaiNguon(); // Xóa loại nguồn
        }
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {
    }
}
Lớp Quản Lý Nguồn Dữ Liệu:
public class QuanLyNguonDuLieu {
    private static final ThreadLocal<String> LOAI_NGUON = ThreadLocal.withInitial(() -> "master");

    public static void setLoaiNguon(String loaiNguon) {
        LOAI_NGUON.set(loaiNguon);
    }

    public static String getLoaiNguon() {
        return LOAI_NGUON.get();
    }

    public static void xoaLoaiNguon() {
        LOAI_NGUON.remove();
    }
}

4. Các Đối Tượng Quan Trọng

Trong MyBatis Interceptor, `Invocation`, `SystemMetaObject`, `MetaObject` và `BoundSql` là những thành phần cốt lõi, đóng vai trò khác nhau trong logic chặn.

1. Invocation

Vai Trò: `Invocation` là tham số của phương thức `intercept()`, đóng gói tất cả thông tin liên quan đến cuộc gọi phương thức bị chặn, bao gồm phương thức mục tiêu, tham số, đối tượng proxy. Thông qua nó, interceptor có thể:
  • Lấy Phương Thức Mục Tiêu: `invocation.getMethod()` trả về phương thức bị chặn (ví dụ: `Executor.query()`).
  • Chỉnh Sửa Tham Số: `invocation.getArgs()` lấy mảng tham số, có thể sửa giá trị tham số (ví dụ: thêm `trangHienTai` và `kichThuocTrang` trong interceptor phân trang).
  • Kiểm Soát Luồng Thực Thi:
    • `invocation.proceed()`: Gọi phương thức gốc (tiếp tục thực thi SQL).
    • Nếu không gọi `proceed()`, thì chặn phương thức gốc (ví dụ: interceptor cache trả lại kết quả cache).

2. SystemMetaObject và MetaObject

Vai Trò: `MetaObject` là công cụ của MyBatis, cho phép truy cập và sửa đổi an toàn các thuộc tính đối tượng (bao gồm thuộc tính private) thông qua cơ chế phản chiếu. `SystemMetaObject` là lớp triển khai nội bộ của `MetaObject`, thường được lấy thông qua `SystemMetaObject.forObject()`. Các Phương Pháp Chính:
  • `getValue(String ten)`:
  • `setValue(String ten, Object giaTri)`:
  • `hasGetter(String ten)`:

3. BoundSql

Vai Trò: `BoundSql` là đối tượng MyBatis bao gói câu lệnh SQL và thông tin tham số, thường được sử dụng trong interceptor để:
  • Lấy Câu Lệnh SQL Gốc: `boundSql.getSql()`.
  • Lấy Đối Tượng Tham Số: `boundSql.getParameterObject()`.
  • Lấy Ánh Xạ Tham Số: `boundSql.getParameterMappings()` (ánh xạ tên và kiểu tham số).

5. Lưu Ý Quan Trọng

  1. Tác Động Đến Hiệu Suất: Interceptor có thể làm tăng chi phí, tránh thực hiện các tính toán phức tạp trong interceptor.
  2. An Toàn Luồng: Đảm bảo logic interceptor là an toàn luồng (ví dụ: sử dụng `ThreadLocal` quản lý ngữ cảnh).
  3. Thứ Tự Thực Hiện: Thứ tự thực hiện của nhiều interceptor tuân theo thứ tự cấu hình, cần lên kế hoạch hợp lý.
  4. Tránh Lạm Dụng: Chỉ sử dụng interceptor trong các trường hợp cần thiết (ví dụ: giám sát, phân trang, kiểm soát quyền dữ liệu), tránh thiết kế quá mức.

Thẻ: mybatis Interceptor JavaDynamicProxy SQLExecution DatabasePerformance

Đăng vào ngày 24 tháng 5 lúc 22:19