Triển khai phân trang hiệu quả với MyBatis-Plus trong ứng dụng Spring Boot

MyBatis-Plus cung cấp cơ chế phân trang vật lý tích hợp sẵn, loại bỏ nhu cầu viết thủ công các câu lệnh SQL có LIMIT, OFFSET hay ROWNUM. Cơ chế này hoạt động dựa trên lớp Page<T> và bộ cắm phân trang (PaginationInnerInterceptor), hỗ trợ tự động sinh câu truy vấn phù hợp với nhiều hệ quản trị cơ sở dữ liệu như MySQL, PostgreSQL, Oracle và SQL Server.

1. Thiết lập bộ cắm phân trang (MP ≥ 3.4.0)

Từ phiên bản 3.4.0, cấu hình phân trang được chuẩn hóa thông qua MybatisPlusInterceptor. Đây là cách duy nhất được khuyến nghị:
@Configuration
@MapperScan("com.acme.repository")
public class PersistenceConfig {

    @Bean
    public MybatisPlusInterceptor configurePaginationInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        // Chỉ định loại cơ sở dữ liệu để MP sinh câu LIMIT đúng chuẩn
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        return interceptor;
    }
}
Lưu ý: Giá trị DbType phải khớp với hệ CSDL thực tế — ví dụ DbType.POSTGRE_SQL cho PostgreSQL hoặc DbType.ORACLE cho Oracle.

2. Thực hiện truy vấn phân trang

a) Qua lớp Service (ưu tiên)

Khi kế thừa IService<Entity>, bạn có thể gọi trực tiếp phương thức page():
Page<Product> request = new Page<>(2, 15); // trang thứ 2, mỗi trang 15 bản ghi

QueryWrapper<Product> filter = new QueryWrapper<>();
filter.gt("price", 100).like("name", "smart");

IPage<Product> response = productService.page(request, filter);

b) Qua lớp Mapper (ít phổ biến hơn)

Nếu sử dụng trực tiếp BaseMapper, hãy dùng selectPage():
Page<Product> page = new Page<>(1, 20);
IPage<Product> result = productMapper.selectPage(page, null);
> ⚠️ Lưu ý: Việc gọi từ IService được ưu tiên vì nó đảm bảo tính toàn vẹn với các tính năng mở rộng như tự động điền trường (AutoFill), xóa mềm (Logic Delete) và kiểm soát quyền truy cập.

3. Hiểu rõ đối tượng Page<T>

Cấu tạo đầy đủ của Page gồm ba tham số bắt buộc:
  • current: số trang bắt đầu từ 1 (không phải 0)
  • size: số bản ghi trên mỗi trang
  • searchCount: bật/tắt việc đếm tổng số bản ghi (mặc định true)
Phương thứcMô tả
getCurrent()Lấy số trang hiện tại
getSize()Lấy kích thước mỗi trang
getTotal()Tổng số bản ghi (chỉ có giá trị nếu searchCount == true)
getPages()Tổng số trang dựa trên totalsize
getRecords()Danh sách dữ liệu của trang hiện tại
setSearchCount(false)Vô hiệu hóa truy vấn đếm — tăng hiệu năng khi không cần hiển thị "tổng số"
Ví dụ tắt đếm tổng bản ghi:
Page<Order> query = new Page<>(1, 50);
query.setSearchCount(false); // Không thực thi SELECT COUNT(...)
orderService.page(query);

4. Phân trang với truy vấn tùy chỉnh (JOIN / GROUP BY)

Khi cần thực hiện truy vấn phức tạp (ví dụ: JOIN nhiều bảng hoặc tính toán tổng hợp), bạn nên định nghĩa phương thức riêng trong Mapper:

a) Khai báo phương thức trong interface

public interface OrderMapper extends BaseMapper<Order> {
    IPage<OrderSummary> fetchOrdersWithCustomer(
        Page<?> page,
        @Param("status") String status
    );
}

b) Triển khai trong file XML — chỉ phần SELECT

<select id="fetchOrdersWithCustomer" resultType="com.acme.dto.OrderSummary">
  SELECT 
    o.id AS orderId,
    o.total_amount,
    c.full_name AS customerName,
    COUNT(i.item_id) AS itemCount
  FROM orders o
  INNER JOIN customers c ON o.customer_id = c.id
  LEFT JOIN order_items i ON o.id = i.order_id
  WHERE o.status = #{status}
  GROUP BY o.id, c.full_name, o.total_amount
</select>
> ✅ Yêu cầu bắt buộc: > - Tham số đầu tiên của phương thức phải là Page<?>. > - Không thêm bất kỳ mệnh đề phân trang nào (LIMIT, OFFSET, ROWNUM) — MyBatis-Plus sẽ tự chèn vào sau khi phân tích. > - Nếu cần đếm chính xác khi dùng GROUP BY, khai báo thêm phương thức đếm riêng có hậu tố _count.

c) Phương thức đếm tùy chỉnh (tùy chọn)

<select id="fetchOrdersWithCustomer_count" resultType="long">
  SELECT COUNT(DISTINCT o.id)
  FROM orders o
  INNER JOIN customers c ON o.customer_id = c.id
  WHERE o.status = #{status}
</select>

5. Xử lý sự cố thường gặp

  • Phân trang không hoạt động: Kiểm tra lại việc đăng ký MybatisPlusInterceptor và đảm bảo không gọi list() thay vì page().
  • Kết quả getTotal() sai: Khi dùng GROUP BY hoặc DISTINCT, hãy cung cấp phương thức _count riêng.
  • Hiệu năng chậm với dữ liệu lớn: Vô hiệu hóa searchCount hoặc cân nhắc phân trang dựa trên con trỏ (cursor-based pagination).

6. Định dạng phản hồi JSON tiêu chuẩn

Cấu trúc trả về mặc định khi chuyển IPage sang JSON:
{
  "records": [...],
  "total": 1247,
  "size": 20,
  "current": 3,
  "pages": 63,
  "searchCount": true
}
Frontend thường chỉ cần xử lý các trường: records, total, current, size.

7. Lưu ý khi dùng đa nguồn dữ liệu

Nếu ứng dụng sử dụng đa nguồn CSDL (ví dụ: dynamic-datasource-spring-boot-starter), hãy đảm bảo MybatisPlusInterceptor được gắn vào đúng SqlSessionFactory tương ứng — hầu hết framework đa nguồn đều xử lý điều này tự động nếu cấu hình đúng.

Thẻ: MyBatis-Plus spring-boot pagination

Đăng vào ngày 13 tháng 7 lúc 03:41