Bỏ qua

Vòng đời một lời gọi Mapper (Call Flow)

So sánh luồng thực thi chi tiết của cùng một phương thức mapper:

User u = mapper.findById(42);

Luồng thực thi trong MyBatis truyền thống

sequenceDiagram
    autonumber
    participant App
    participant Proxy as MapperProxy<br/>(JDK dynamic proxy)
    participant Method as MapperMethod
    participant Cfg as Configuration
    participant SQL as SqlSource / OGNL
    participant Exec as Executor
    participant Res as ResultSetHandler
    App->>Proxy: findById(42)
    Proxy->>Method: invoke — tra cứu Method → MapperMethod
    Method->>Cfg: getMappedStatement("…findById")
    Method->>SQL: getBoundSql(param)
    SQL-->>Method: Câu SQL + ParameterMappings
    Method->>Exec: query(ms, param, RowBounds, handler)
    Exec->>Exec: createCacheKey — reflection trên các tham số
    Exec->>Exec: setParameters — tra cứu TypeHandler cho mỗi tham số
    Exec->>Res: handleResultSets(rs)
    loop Mỗi dòng (Row)
        loop Mỗi cột (Column)
            Res->>Res: metaObject.setValue(property, value) qua Reflection
        end
    end
    Res-->>App: User POJO

MyBatis phải thực hiện reflection ở 4 giai đoạn: createCacheKey, getBoundSql (OGNL cho dynamic SQL), setParametershandleResultSets. Trong đó, handleResultSets tiêu tốn nhiều CPU và bộ nhớ nhất vì chạy lặp qua từng cột của từng dòng dữ liệu.

Luồng thực thi trong LarkBatis

sequenceDiagram
    autonumber
    participant App
    participant Impl as UserMapper$$Impl<br/>(Class Java thật)
    participant Sess as LarkBatisSession
    participant JDBC as PreparedStatement
    participant Row as UserRow
    App->>Impl: findById(42)
    Impl->>Sess: conn()
    Sess-->>Impl: Connection (từ transaction hoặc pool)
    Impl->>JDBC: prepareStatement(SQL_findById)
    Impl->>JDBC: setLong(1, 42)
    Impl->>JDBC: executeQuery()
    JDBC-->>Impl: ResultSet
    Impl->>Row: read(rs)
    Row-->>Impl: User POJO
    Impl->>Sess: release(c)
    Impl-->>App: User POJO

Không dynamic proxy, không tra cứu hash map, không reflection. Mọi thông tin cấu trúc đã được giải quyết từ lúc build và nhúng thẳng dưới dạng hằng số hoặc lệnh gọi Java trực tiếp.

So sánh chi tiết phép gán dữ liệu từng cột

Với mỗi cột trên mỗi dòng, MyBatis thực hiện:

metaObject.setValue("userName", v)
// 1. new PropertyTokenizer(name)   -> Cấp phát heap object + indexOf + substring
// 2. BeanWrapper.set(prop, value)
// 3. metaClass.getSetInvoker(name) -> Tra cứu HashMap theo String
// 4. Object[] params = { value }   -> Cấp phát mảng Object[]
// 5. method.invoke(obj, params)    -> Gọi qua Java Reflection

Trong khi LarkBatis thực hiện:

u.setUserName(rs.getString(4));
// 1. Tên property đã được resolve lúc build
// 2. Chỉ số cột (4) đã cố định lúc build
// 3. Thực thi trực tiếp bằng một lệnh bytecode putfield

Với kết quả 10 cột × 1.000 dòng, MyBatis tạo ra: - 10.000 đối tượng PropertyTokenizer - 10.000 mảng Object[] - 10.000 lần tra cứu HashMap - 10.000 lần gọi Method.invoke()

Chuỗi gọi sâu setValue → BeanWrapper → Invoker khiến JIT Compiler không thể áp dụng Escape Analysis để loại bỏ việc cấp phát bộ nhớ heap.

Các thao tác thực tế diễn ra lúc Runtime trong LarkBatis

Thao tác runtime Mục đích
s.conn() / s.release(c) Mượn connection từ transaction hoặc connection pool
ps.setLong(1, 42) Bind giá trị tham số vào PreparedStatement
ps.executeQuery() Gửi truy vấn xuống database qua JDBC driver
rs.getString(2) Đọc dữ liệu từ driver buffer
Đánh giá biến boolean và StringBuilder.append() Ghép chuỗi đối với dynamic SQL
trackVariants() Kiểm soát số lượng biến thể SQL trong cache

Toàn bộ các tầng trung gian trừu tượng đã bị loại bỏ hoàn toàn.

Tác động hiệu năng theo quy mô dữ liệu

  • Truy vấn đơn bản ghi (Single row lookup): Thời gian phần lớn nằm ở mạng và database I/O, mức chênh lệch giữa hai framework là không đáng kể.
  • Truy vấn danh sách lớn (Reports, Export, Batch): Với 10.000 dòng (40.000 cột), việc loại bỏ reflection và cấp phát heap giúp giảm thời gian xử lý CPU từ 3,0 ms xuống 0,8 ms và giảm áp lực GC rõ rệt.