MyBatis-Plus分頁失效問題的完整解決方案
一、問題描述
使用 PageHelper.startPage() 配合 MyBatis-Plus 的 BaseMapper.selectList(Wrapper) 進(jìn)行分頁查詢時,實(shí)際執(zhí)行的 SQL 沒有 LIMIT 子句,返回了全部數(shù)據(jù)。
// 期望生成:SELECT * FROM table WHERE ... LIMIT 0, 50 // 實(shí)際生成:SELECT * FROM table WHERE ...(無分頁) PageHelper.startPage(1, 50); List<Entity> list = mapper.selectList(wrapper);
二、兩種分頁方案對比
2.1 PageHelper(第三方分頁插件)
- 來源:
com.github.pagehelper:pagehelper - 原理:通過 MyBatis Interceptor 攔截 SQL,自動追加
LIMIT - 配合對象:
PageInfo包裝分頁結(jié)果
PageHelper.startPage(pageNum, pageSize); List<User> list = userMapper.selectByCondition(param); PageInfo<User> pageInfo = new PageInfo<>(list);
2.2 MyBatis-Plus 內(nèi)置分頁
- 來源:
com.baomidou:mybatis-plus內(nèi)置 - 原理:通過
PaginationInnerInterceptor攔截 SQL - 配合對象:
Page<T>作為參數(shù)傳入
Page<User> page = new Page<>(pageNum, pageSize); Page<User> result = userMapper.selectPage(page, wrapper);
三、沖突原因深度分析
3.1 PageHelper 的工作原理
① PageHelper.startPage(1, 50) → 將分頁參數(shù)存入 ThreadLocal ② mapper.selectXxx() → MyBatis 執(zhí)行 SQL 前,PageHelper 的 Interceptor 檢查 ThreadLocal → 發(fā)現(xiàn)有分頁參數(shù),改寫 SQL 追加 LIMIT → 執(zhí)行完畢后,清除 ThreadLocal 中的分頁參數(shù) ③ new PageInfo<>(list) → 從 list(被 PageHelper 包裝過的 Page 對象)中取分頁信息
關(guān)鍵點(diǎn):PageHelper 只會攔截 startPage() 之后的下一條 SQL 執(zhí)行。
3.2 為什么與 MyBatis-Plus 的 selectList 沖突
MyBatis-Plus 的 BaseMapper.selectList(Wrapper) 不是簡單的一次 SQL 調(diào)用。其內(nèi)部流程:
mapper.selectList(wrapper) ↓ MyBatis-Plus 動態(tài)構(gòu)建 SQL(通過 AbstractWrapper 生成 WHERE 條件) ↓ 調(diào)用 MyBatis 的 SqlSession.selectList() ↓ MyBatis Executor 執(zhí)行
沖突發(fā)生的幾種可能原因:
| 原因 | 說明 |
|---|---|
| Interceptor 執(zhí)行順序 | PageHelper 的 Interceptor 和 MyBatis-Plus 的 Interceptor 注冊順序不當(dāng),導(dǎo)致 PageHelper 沒有正確攔截到最終的 SQL |
| ThreadLocal 被提前消費(fèi) | 如果在 startPage() 和 selectList() 之間有其他 SQL 執(zhí)行(如條件構(gòu)造時觸發(fā)了查詢),分頁參數(shù)已被消費(fèi) |
| SQL 改寫時機(jī)不匹配 | MyBatis-Plus 構(gòu)造的 SQL 在 PageHelper 攔截時還未最終確定,導(dǎo)致 LIMIT 沒有追加到正確的位置 |
| 版本兼容性 | 不同版本的 PageHelper 和 MyBatis-Plus 對 Interceptor SPI 的實(shí)現(xiàn)有差異 |
3.3 PageHelper 的 ThreadLocal 機(jī)制詳解
public class PageHelper {
// 每個線程獨(dú)立的分頁參數(shù)
private static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal<>();
public static <E> Page<E> startPage(int pageNum, int pageSize) {
Page<E> page = new Page<>(pageNum, pageSize);
LOCAL_PAGE.set(page); // 存入當(dāng)前線程
return page;
}
}
// Interceptor 中:
public class PageInterceptor implements Interceptor {
public Object intercept(Invocation invocation) {
Page page = PageHelper.LOCAL_PAGE.get();
if (page != null) {
// 改寫 SQL 追加 LIMIT
PageHelper.LOCAL_PAGE.remove(); // 用完立即清除
// ...執(zhí)行帶 LIMIT 的 SQL
}
return invocation.proceed();
}
}
問題核心:如果 LOCAL_PAGE.get() 在錯誤的時機(jī)被調(diào)用(SQL 還沒準(zhǔn)備好),或者在正確時機(jī)沒被調(diào)用(Interceptor 順序靠后),分頁就失效。
四、MyBatis-Plus 分頁的正確用法
4.1 注冊分頁插件(必須)
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
PaginationInnerInterceptor pagination = new PaginationInnerInterceptor(DbType.MYSQL);
pagination.setMaxLimit(500L); // 單頁最大條數(shù)限制
interceptor.addInnerInterceptor(pagination);
return interceptor;
}
}
沒有這個配置,selectPage 方法的分頁參數(shù)會被忽略,等同于 selectList。
4.2 使用 selectPage
// 創(chuàng)建分頁對象(頁碼從1開始) Page<Entity> page = new Page<>(pageNum, pageSize); // 執(zhí)行分頁查詢 Page<Entity> result = mapper.selectPage(page, wrapper); // 獲取結(jié)果 List<Entity> records = result.getRecords(); // 當(dāng)前頁數(shù)據(jù) long total = result.getTotal(); // 總記錄數(shù) long current = result.getCurrent(); // 當(dāng)前頁碼 long size = result.getSize(); // 每頁條數(shù) long pages = result.getPages(); // 總頁數(shù)
4.3 生成的 SQL
-- MyBatis-Plus 分頁實(shí)際執(zhí)行兩條 SQL: -- 第一條:查總數(shù) SELECT COUNT(*) AS total FROM user WHERE status = 1 -- 第二條:查數(shù)據(jù)(帶 LIMIT) SELECT id, name, status FROM user WHERE status = 1 LIMIT 0, 50
4.4 優(yōu)化:不需要總數(shù)時跳過 COUNT
// 第三個參數(shù) false 表示不查詢總數(shù)(提升性能) Page<Entity> page = new Page<>(pageNum, pageSize, false);
五、PageHelper 和 MyBatis-Plus 分頁能否共存
可以共存,但需要明確分工:
| 場景 | 推薦方案 |
|---|---|
使用 MyBatis-Plus 的 BaseMapper 方法(selectList/selectPage) | 用 MyBatis-Plus 分頁 |
| 使用自定義 XML 中的 SQL(如復(fù)雜 JOIN 查詢) | 用 PageHelper 或 MyBatis-Plus 分頁都可以 |
| 混合使用 | 按方法區(qū)分,不要在同一方法中混用 |
原則:一個查詢方法中只使用一種分頁方式,不要混用。
六、完整示例
6.1 Entity
package com.example.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import java.util.Date;
import lombok.Data;
@Data
@TableName("product")
public class Product {
@TableId(type = IdType.AUTO)
private Long id;
private String code;
private String name;
private String category;
private Integer price;
private Integer status;
private Date createTime;
}
6.2 Mapper
package com.example.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.entity.Product;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface ProductMapper extends BaseMapper<Product> {
}
6.3 分頁插件配置
package com.example.config;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
PaginationInnerInterceptor pagination = new PaginationInnerInterceptor(DbType.MYSQL);
pagination.setMaxLimit(500L); // 防止惡意大分頁
pagination.setOverflow(false); // 頁碼超出總頁數(shù)時不自動回到第一頁
interceptor.addInnerInterceptor(pagination);
return interceptor;
}
}
6.4 Service
package com.example.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.example.entity.Product;
import com.example.mapper.ProductMapper;
import jakarta.annotation.Resource;
import java.util.HashMap;
import java.util.Map;
import org.springframework.stereotype.Service;
@Service
public class ProductService {
@Resource
private ProductMapper productMapper;
/**
* 分頁查詢商品列表.
*
* @param name 商品名稱(模糊搜索)
* @param category 分類(精準(zhǔn)搜索)
* @param status 狀態(tài)
* @param pageNum 頁碼(從1開始)
* @param pageSize 每頁條數(shù)
* @return 分頁結(jié)果
*/
public Map<String, Object> listProducts(
String name, String category, Integer status,
int pageNum, int pageSize) {
// 1. 構(gòu)造查詢條件
LambdaQueryWrapper<Product> wrapper = new LambdaQueryWrapper<>();
wrapper.like(name != null && !name.isBlank(), Product::getName, name);
wrapper.eq(category != null && !category.isBlank(), Product::getCategory, category);
wrapper.eq(status != null, Product::getStatus, status);
wrapper.orderByDesc(Product::getCreateTime);
// 2. 創(chuàng)建分頁對象
Page<Product> page = new Page<>(pageNum, pageSize);
// 3. 執(zhí)行分頁查詢(會自動執(zhí)行 COUNT + SELECT LIMIT)
Page<Product> pageResult = productMapper.selectPage(page, wrapper);
// 4. 組裝返回結(jié)果
Map<String, Object> result = new HashMap<>();
result.put("list", pageResult.getRecords()); // 當(dāng)前頁數(shù)據(jù)
result.put("total", pageResult.getTotal()); // 總記錄數(shù)
result.put("pageNum", pageResult.getCurrent()); // 當(dāng)前頁碼
result.put("pageSize", pageResult.getSize()); // 每頁條數(shù)
result.put("pages", pageResult.getPages()); // 總頁數(shù)
return result;
}
}
6.5 Controller
package com.example.controller;
import com.example.service.ProductService;
import jakarta.annotation.Resource;
import java.util.Map;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/product")
public class ProductController {
@Resource
private ProductService productService;
@GetMapping("/list")
public Map<String, Object> list(
@RequestParam(required = false) String name,
@RequestParam(required = false) String category,
@RequestParam(required = false) Integer status,
@RequestParam(defaultValue = "1") int pageNum,
@RequestParam(defaultValue = "10") int pageSize) {
return productService.listProducts(name, category, status, pageNum, pageSize);
}
}
6.6 執(zhí)行效果
請求:
GET /api/product/list?category=電子產(chǎn)品&pageNum=2&pageSize=10
實(shí)際執(zhí)行的 SQL(可在日志中看到):
-- 第一條:查總數(shù) SELECT COUNT(*) AS total FROM product WHERE category = '電子產(chǎn)品' -- 第二條:查數(shù)據(jù) SELECT id, code, name, category, price, status, create_time FROM product WHERE category = '電子產(chǎn)品' ORDER BY create_time DESC LIMIT 10, 10
返回:
{
"list": [...],
"total": 156,
"pageNum": 2,
"pageSize": 10,
"pages": 16
}七、PaginationInnerInterceptor 配置項(xiàng)
| 配置項(xiàng) | 默認(rèn)值 | 說明 |
|---|---|---|
dbType | 必填 | 數(shù)據(jù)庫類型,影響 LIMIT 語法生成 |
maxLimit | 無限制 | 單頁最大條數(shù)(防止 pageSize=999999 惡意請求) |
overflow | false | 頁碼超出時:true=自動回第一頁,false=返回空數(shù)據(jù) |
optimizeJoin | true | 優(yōu)化 COUNT SQL(去掉不必要的 JOIN 和 ORDER BY) |
八、常見問題排查清單
| 現(xiàn)象 | 可能原因 | 解決方案 |
|---|---|---|
| SQL 完全沒有 LIMIT | 沒注冊 PaginationInnerInterceptor | 添加 MybatisPlusConfig 配置類 |
| total 始終為 0 | 數(shù)據(jù)庫方言設(shè)置錯誤 | 確認(rèn) DbType.MYSQL 與實(shí)際數(shù)據(jù)庫匹配 |
| 返回全部數(shù)據(jù) | 用了 selectList 而非 selectPage | 改用 selectPage 并傳入 Page 對象 |
| PageHelper 時好時壞 | startPage 和查詢之間有其他 SQL | 確保 startPage 緊跟查詢,或改用 MyBatis-Plus 分頁 |
| 分頁參數(shù)被忽略 | Page 對象沒傳給 selectPage | 檢查方法簽名,確認(rèn) page 是第一個參數(shù) |
九、總結(jié)
選型建議:
使用 MyBatis-Plus 的項(xiàng)目 → 優(yōu)先用 MyBatis-Plus 內(nèi)置分頁(selectPage + PaginationInnerInterceptor)
? 與 MyBatis-Plus 的 Wrapper 體系天然兼容
? 不依賴 ThreadLocal,無執(zhí)行順序隱患
? 支持多數(shù)據(jù)庫方言
使用原生 MyBatis + XML 的項(xiàng)目 → 用 PageHelper
? 對已有代碼侵入小
? 不需要修改 Mapper 方法簽名
混合項(xiàng)目 → 不要在同一個方法中混用兩種分頁
以上就是MyBatis-Plus分頁失效問題的完整解決方案的詳細(xì)內(nèi)容,更多關(guān)于MyBatis-Plus分頁失效問題的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Java之SpringCloud Eurka注冊錯誤解決方案
這篇文章主要介紹了Java之SpringCloud Eurka注冊錯誤解決方案,本篇文章通過簡要的案例,講解了該項(xiàng)技術(shù)的了解與使用,以下就是詳細(xì)內(nèi)容,需要的朋友可以參考下2021-07-07
Java List集合返回值去掉中括號(''[ ]'')的操作
這篇文章主要介紹了Java List集合返回值去掉中括號('[ ]')的操作,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-08-08
SpringBoot Redis 啟動失敗深度剖析與標(biāo)準(zhǔn)化解決方案(快速定位問題)
本文基于SpringBoot 2.x/3.x(含Jakarta EE適配)與Redis 6.x/7.x生態(tài),系統(tǒng)梳理12類常見錯誤,涵蓋錯誤現(xiàn)象、深層原因、解決方案及最佳實(shí)踐,助力開發(fā)者快速定位問題,感興趣的朋友跟隨小編一起看看吧2025-11-11
xxl-job定時任務(wù)配置應(yīng)用及添加到springboot項(xiàng)目中實(shí)現(xiàn)動態(tài)API調(diào)用
XXL-JOB是一個分布式任務(wù)調(diào)度平臺,其核心設(shè)計(jì)目標(biāo)是開發(fā)迅速、學(xué)習(xí)簡單、輕量級、易擴(kuò)展,本篇文章主要是對xuxueli的xxl-job做一個簡單的配置,以及將其添加到自己已有的項(xiàng)目中進(jìn)行api調(diào)用,感興趣的朋友跟隨小編一起看看吧2024-04-04
Spring?Cloud?Alibaba微服務(wù)組件Sentinel實(shí)現(xiàn)熔斷限流
這篇文章主要為大家介紹了Spring?Cloud?Alibaba微服務(wù)組件Sentinel實(shí)現(xiàn)熔斷限流過程示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-06-06
Spring @Transactional注解的聲明式事務(wù)簡化業(yè)務(wù)邏輯中的事務(wù)管理
這篇文章主要為大家介紹了Spring @Transactional注解的聲明式事務(wù)簡化業(yè)務(wù)邏輯中的事務(wù)管理,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-10-10

