若依 + MyBatis-Plus,分页为什么悄悄失效了?

接手若依项目,想给列表页加个 MyBatis-Plus 的 LambdaQueryWrapper 条件查询------毕竟 eq()like() 链式写法比手写 XML 舒服太多,于是引入 MyBatis-Plus。代码跑起来,列表能出数据,可一翻页就出问题:要么第 2 页和第 1 页一模一样,要么 total 永远是 0,要么干脆把整张表全捞出来。

最要命的是:它不报错。就是悄悄查错,等你上线了才发现列表接口把几万行全查出来。

先说结论:分页失效几乎都是同一个原因------若依默认的 PageHelper 分页,和 MyBatis-Plus 的分页拦截器,两个机制在抢同一条 SQL 的分页权

一、若依默认的分页是怎么做的

若依生成的列表接口,标准写法是 startPage() + getDataTable(list)

bash 复制代码
@PostMapping("/list")
@ResponseBody
public TableDataInfo list(SysUser user) {
    // startPage 配合前端完成自动分页
    startPage();
    List<SysUser> list = userService.selectUserList(user);
    return getDataTable(list);
}

背后是 PageHelper 。startPage() 干的事,是 new 一个 Page 对象塞进 ThreadLocal ;等 MyBatis 执行下一条 SQL 时,PageHelper 的拦截器把这个 Page 拿出来拼上 LIMIT,再把 total 查出来,getDataTable 包成 {total, rows} 返回。

这套东西在纯 MyBatis 环境里没问题。但它的命门就藏在 ThreadLocal 里。

二、一上 MyBatis-Plus,为什么就悄悄失效了

两个坑,都跟这个 ThreadLocal 有关。

坑一:Page 没被消费,污染下一条 SQL。

startPage() 创建 Page 之后,如果后面那条 SQL 因为分支判断没执行(比如 if (user != null) 里才查,user 为 null 就跳过),这个 Page 就留在 ThreadLocal 里没人消费。等下一次请求复用这个线程,第一条 SQL 会被意外套上分页------数据莫名其妙变少,甚至报 LIMIT 语法错误。

坑二:两个分页插件互相抢 SQL。

MyBatis-Plus 自己也带一个分页拦截器 PaginationInnerInterceptor,靠识别 selectPage(new Page<>(current, size), ...) 里的 IPage 参数来拼 LIMIT。若依的 PageHelper 靠 ThreadLocal 里的 Page 来拼。两个拦截器同时挂在 MyBatis 的执行链上,同一个查询被拼两次分页,或者 MP 的分页被 PageHelper 残留的 Page 干扰,结果就是错乱的。

一句话:PageHelper 和 MyBatis-Plus 的分页,只能留一个。 混用必失效,而且是静默失效------这也是它最坑的地方。

三、正确姿势:彻底移除 PageHelper,分页收口到基 类

我的做法就一条原则:分页这件事,全项目只写一次

第一步,全局只留 MyBatis-Plus 的分页 插件 :

bash 复制代码
@Configuration
public class MybatisPlusConfig {

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        // 只保留 MP 自己的分页插件,MySQL 方言
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        // 若项目需要多租户行级隔离,再追加 TenantLineInnerInterceptor
        return interceptor;
    }
}

第二步,分页参数统一收口到一个基类,每个查询 DTO 继承它,默认值给好:

bash 复制代码
@Data
public class BaseQuery {

    // 当前页,从 1 开始
    private long current = 1;

    // 每页大小
    private long size = 10;
}

第三步,service 层同样抽一个基类,分页逻辑只写一次:

bash 复制代码
public abstract class BaseServiceImpl<M extends BaseMapper<E>, E extends BaseEntity,
        Q extends BaseQuery, C, U extends BaseUpdateRequest>
        extends ServiceImpl<M, E> {

    // 子类只实现查询条件构造
    protected abstract LambdaQueryWrapper<E> buildWrapper(Q query);

    public IPage<E> page(Q query) {
        // 分页参数直接进 Page 对象,绕开 ThreadLocal 那套
        return baseMapper.selectPage(new Page<>(query.getCurrent(), query.getSize()), buildWrapper(query));
    }
}

这里 CU 是新增/更新 DTO 的泛型(与分页无关,一并列出保持签名完整)。Controller 直接调 page(),返回 ApiDataResult<IPage<E>>

bash 复制代码
@RestController
@RequestMapping("/system/user")
public class SysUserController {

    private final SysUserService userService;

    @GetMapping("/page")
    public ApiDataResult<IPage<SysUser>> page(SysUserQuery query) {
        // 返回结构:ApiDataResult { code, msg, data: IPage{records, total} }
        return ApiDataResult.success(userService.page(query));
    }
}

selectPage 返回的 IPage 自带 records(本页数据)和 total(总数)。前端这边注意:res 是请求封装返回的 ApiDataResult(axios 已解包一层 response.data),所以 res.data 才是 IPagerecordstotal 挂在它下面:

bash 复制代码
const res = await pageSysUser(params)
list.value = res.data.records
total.value = res.data.total

四、配套:通用 CRUD 基类 + 生成器,让新模块只写差异

分页收口到基类之后,还能再进一步:新增、更新、删除、状态切换这些通用逻辑也一并收进基类,子类只剩两处差异代码------buildWrapper()(查询条件)和 newEntity()(new 一个实体)。

于是每新增一个业务模块, 代码生成 器只要吐出几十行差异代码,分页、CRUD 全都不用重写。这比若依默认生成的那一套 controller + service + impl + mapper 的复制粘贴干净得多,也彻底没有「每个模块各写一遍分页」的重复劳动。

bash 复制代码
@Override
protected LambdaQueryWrapper<SysUser> buildWrapper(SysUserQuery query) {
    return new LambdaQueryWrapper<SysUser>()
            // 有值才拼条件,避免把空字符串当查询条件
            .like(StringUtils.hasText(query.getName()), SysUser::getName, query.getName())
            .orderByDesc(SysUser::getCreateTime);
}

五、两个细节提醒

  1. 默认值一定给current = 1size = 10 写死在基类里,前端不传也能翻出第一页,接口不会因为缺参数直接报错。
  2. 前端翻页记得重置页码 :点「查询」时把 current 归 1 再请求,否则在第 5 页点搜索,条件变了还停在第 5 页,很容易出现「翻页了但数据没变」的误判。

收尾

若依 + MyBatis-Plus 分页失效,本质不是「哪个插件更好」,而是两个分页机制都在抢同一条 SQL,抢来抢去就错了。解决思路就一条:分页只留一个入口,参数收口、逻辑收口,全项目只写一次。

如果你也在这条路上踩过坑,欢迎评论区聊聊:你是直接弃了 PageHelper,还是想办法让两个插件共存了?

相关推荐
yxlalm1 小时前
4.java秒杀项目第四课-后端商品后台接口
java·开发语言
掘金者阿豪1 小时前
OceanBase 和金仓怎么选?别只看分布式,复杂查询更考验架构取舍
前端·后端·架构
宁之明起(B服)1 小时前
vulhub靶场(tomcat三件套:PUT写马、Ghostcat与manager弱口令)
java·tomcat
cjz38991 小时前
Redis + Lua 实现秒杀优惠券 IP + 用户双维度令牌桶限流
后端
Shinomiya1 小时前
虚拟地址空间:从“地址一样但物理内存不同”说起
后端
孙启超1 小时前
【AI开发之Rust】第 1 课:认识 Rust 与开发环境 —— 装好工具链,跑通第一个程序
开发语言·人工智能·后端·ai·rust·安全架构
君顾11 小时前
家政派单小程序源码开发实战:从零搭建到上线全流程指南
java·开发语言·家政派单
流烟默1 小时前
HTTP 连接管理技术梳理:JDK KeepAliveCache 机制与池化选型
java·网络协议·http
阿酷tony2 小时前
视频专栏列表的防录屏水印和跑马灯效果(也可网站调用)
java·前端·音视频