Kỹ thuật Lazy Loading và Cơ chế Cache trong MyBatis

1. Cơ chế Tải chậm (Lazy Loading)

Trong MyBatis, resultMap hỗ trợ ánh xạ nâng cao thông qua các thẻ association (quan hệ 1-1) và collection (quan hệ 1-nhiều). Cả hai thẻ này đều tích hợp tính năng Lazy Loading (tải chậm), giúp tối ưu hiệu suất bằng cách chỉ truy vấn dữ liệu liên quan khi thực sự cần thiết.

Giả sử chúng ta có nhu cầu truy vấn thông tin đơn hàng và thông tin khách hàng đi kèm. Thay vì JOIN hai bảng ngay lập tức, chúng ta có thể lấy danh sách đơn hàng trước, sau đó chỉ khi nào gọi đến đối tượng khách hàng thì MyBatis mới thực hiện câu lệnh SQL để lấy dữ liệu đó.

Dưới đây là cấu hình Mapper XML để thực hiện logic này:

<mapper namespace="com.tech.store.mapper.SaleOrderMapper">
    <!-- Định nghĩa ResultMap cho Lazy Loading -->
    <resultMap type="com.tech.store.model.SaleOrder" id="OrderLazyMap">
        <id column="order_id" property="id" />
        <result column="order_code" property="code" />
        <result column="customer_id" property="customerId" />
        
        <!-- 
            select: ID của statement dùng để truy vấn khách hàng.
            column: Cột trong bảng đơn hàng dùng làm tham số cho truy vấn khách hàng (customer_id).
        -->
        <association property="customer" javaType="com.tech.store.model.Customer" 
                     column="customer_id" 
                     select="com.tech.store.mapper.CustomerMapper.getById">
        </association>
    </resultMap>
    
    <select id="getOrdersWithLazyCustomer" resultMap="OrderLazyMap">
        SELECT * FROM sale_orders
    </select>
</mapper>

Để kích hoạt tính năng này, bạn cần cấu hình trong tệp cài đặt toàn cục của MyBatis (settings):

<settings>
   <!-- Bật tính năng tải chậm -->
   <setting name="lazyLoadingEnabled" value="true"/>
   <!-- Tắt chế độ tải tích cực để đảm bảo dữ liệu chỉ được tải khi truy cập -->
   <setting name="aggressiveLazyLoading" value="false"/>
</settings>

Đoạn mã kiểm thử thực tế:

public void testLazyLoading() {
    SqlSession session = factory.openSession();
    SaleOrderMapper mapper = session.getMapper(SaleOrderMapper.class);
    List<SaleOrder> orders = mapper.getOrdersWithLazyCustomer();
    
    for(SaleOrder item : orders) {
        // Lúc này câu lệnh SQL lấy Customer mới được thực thi
        System.out.println(item.getCustomer().getName());
    }
    session.close();
}

2. Cơ chế Cache cấp 1 và Cấp 2

MyBatis cung cấp hệ thống bộ đệm (cache) để giảm tải cho cơ sở dữ liệu và tăng tốc độ phản hồi của ứng dụng.

Cache cấp 1 (L1 Cache)

L1 Cache hoạt động ở phạm vi SqlSession. Mỗi khi một phiên làm việc được tạo ra, MyBatis sẽ đính kèm một cấu trúc HashMap để lưu trữ kết quả truy vấn. Các phiên làm việc khác nhau không chia sẻ dữ liệu này.

  • Khi truy vấn lần đầu, kết quả được lưu vào L1 Cache.
  • Nếu cùng một truy vấn được thực hiện trong cùng session, MyBatis lấy dữ liệu từ cache.
  • Mọi thao tác insert, update, delete hoặc lệnh commit sẽ xóa sạch L1 Cache để tránh dữ liệu rác.
public void testL1Cache() {
    SqlSession session = factory.openSession();
    CustomerMapper mapper = session.getMapper(CustomerMapper.class);
    
    // Lần 1: Truy vấn DB
    Customer c1 = mapper.getById(100);
    
    // Lần 2: Lấy từ bộ nhớ (không phát sinh SQL)
    Customer c2 = mapper.getById(100);
    
    session.close();
}

Cache cấp 2 (L2 Cache)

L2 Cache có phạm vi rộng hơn, hoạt động ở cấp độ Mapper Namespace. Nhiều SqlSession có thể dùng chung dữ liệu trong L2 Cache nếu chúng truy cập cùng một Namespace.

Để sử dụng L2 Cache, cần tuân thủ các bước:

  1. Kích hoạt cacheEnabled trong settings (mặc định là true).
  2. Khai báo thẻ <cache/> trong file Mapper XML.
  3. Các đối tượng POJO phải thực hiện interface Serializable.
<mapper namespace="com.tech.store.mapper.CustomerMapper">
    <!-- Kích hoạt cache cho namespace này -->
    <cache />
    ...
</mapper>

Lưu ý: Dữ liệu chỉ được đẩy vào L2 Cache sau khi SqlSession được đóng lại (close).

3. Tích hợp Ehcache làm bộ đệm ngoài

Trong các hệ thống phân tán hoặc khi cần quản lý cache chuyên sâu, MyBatis cho phép tích hợp các thư viện bên thứ ba như Ehcache.

Sau khi thêm thư viện mybatis-ehcache, bạn cấu hình loại cache trong XML:

<mapper namespace="com.tech.store.mapper.CustomerMapper">
    <cache type="org.mybatis.caches.ehcache.EhcacheCache"/>
</mapper>

Cần chuẩn bị tệp ehcache.xml để định nghĩa các thông số như thời gian sống (TTL), số lượng phần tử tối đa trong bộ nhớ:

<ehcache>
    <diskStore path="D:\cache\data" />
    <defaultCache 
        maxElementsInMemory="2000" 
        eternal="false" 
        timeToIdleSeconds="300"
        timeToLiveSeconds="600" 
        overflowToDisk="true" />
</ehcache>

4. Đánh giá và Ứng dụng

Trường hợp nên dùng: L2 Cache cực kỳ hiệu quả cho các câu lệnh truy vấn phức tạp, tốn tài nguyên nhưng dữ liệu ít khi thay đổi (ví dụ: báo cáo định kỳ, danh mục sản phẩm cố định).

Hạn chế: MyBatis L2 Cache hoạt động theo đơn vị Namespace. Nếu bạn cập nhật một dòng dữ liệu trong bảng sản phẩm, toàn bộ cache của Namespace sản phẩm đó sẽ bị xóa sạch. Điều này khiến nó không phù hợp cho các kịch bản cần quản lý cache ở mức chi tiết (fine-grained).

Thẻ: mybatis Cache lazy-loading Java sql

Đăng vào ngày 15 tháng 8 lúc 17:22