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ức | Mô 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 total và size |
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.