MyBatis‑Plus IPage 分页踩坑实战:分页与全量查询兼容方案

前言

业务开发中经常遇到一种场景:同一个查询逻辑,根据前端传参动态选择分页查询 / 查询全部数据 。很多同学第一想法:复用同一套 Mapper 接口,传入Page(pageNum,-1)实现不分页,一行代码搞定。

但实际项目落地时,经常出现各种诡异问题:设置 size=-1,打印 SQL 依然出现LIMIT;IPage 的 records 为空;拦截器不生效等。尤其在二次封装框架环境下,分页拦截器加载异常会进一步放大问题。

本文基于真实业务实战,给出一套稳定、可维护的生产级实现方案。

环境:SpringBoot + MyBatis‑Plus,自定义 XML 手写 SQL,非 MP 自带 Wrapper 查询。

业务场景

业务需求:查询业务明细,支持两种模式

  1. 分页模式:按照页码、页大小返回 IPage 对象,用于前端表格分页渲染;
  2. 全量模式:不需要分页,查询全部数据,做后续业务计算处理,接口返回结构依然统一返回IPage

最初想法:只写一个 Mapper 方法,通过传入new Page<>(0,-1),依靠 MyBatis‑Plus 内置特性关闭分页,一份 Mapper、一份 XML 搞定两种逻辑。

遇到的现实问题

  1. Page(0,-1)不分页特性强依赖 MyBatis‑Plus 分页拦截器正常注册、生效
  2. 如果项目框架存在拦截器重复 / 缺失、版本差异、XML 手写 LIMIT,size=-1会直接失效,SQL 依旧拼接 LIMIT,只能查到部分数据。
  3. Mapper 接口直接传null给 IPage 参数,会触发空指针异常。
  4. MyBatis 本身不支持 Java 接口重载绑定同一个 XML 的id,同名不同参数重载会抛出 Mapper 绑定异常。

核心结论:生产环境不建议依赖 Page(size=-1) 做全量查询,它属于便捷语法,环境稍有变化就会出线上 bug。

实战最优方案:双 Mapper 方法 + SQL 片段复用

核心思路:

  1. Mapper 层提供两个不同名称的方法:一个带 IPage 用于分页;一个不带 IPage 直接返回 List,用于查询全部;
  2. XML 使用<sql>抽取公共查询片段,两个<select>通过<include>复用同一套查询逻辑,SQL 只维护一份,避免复制粘贴带来的维护灾难;
  3. Service 层做分支判断,全量查询拿到 List 后手动封装为 IPage 对象,保证对外接口返回对象统一。

1、Mapper 接口定义

less 复制代码
public interface BizDataMapper {

    /**
     * 分页查询业务明细
     * @param page MP分页对象
     * @param queryDto 查询条件
     * @return IPage分页结果
     */
    IPage<BizDataDTO> queryBizDetail(IPage<BizDataDTO> page,
                                     @Param("dto") BizQueryDTO queryDto);

    /**
     * 查询全部业务明细(不分页)
     * @param queryDto 查询条件
     * @return 全量数据List
     */
    List<BizDataDTO> queryBizDetailAll(@Param("dto") BizQueryDTO queryDto);
}

注意:两个方法方法名不能相同,MyBatis 依靠 id 与接口方法名绑定,不支持重载。

2、Mapper XML 实现,复用 SQL 片段

关键点:业务查询逻辑抽成<sql>公共片段,两个 select 标签直接引用;分页查询不要手写 limit,交给 MyBatis‑Plus 拦截器自动处理。

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.xxx.mapper.BizDataMapper">

    <!-- 公共查询SQL片段,分页、全量查询共用,业务条件只维护一处 -->
    <sql id="query_biz_detail_common">
        SELECT
            t.*,
            rel.ext_info
        FROM biz_main_table t
        LEFT JOIN biz_rel_table rel ON rel.rel_id = t.rel_id
        WHERE t.group_id = #{dto.groupId}
        <if test="dto.bizStatus != null">
            AND t.biz_status = #{dto.bizStatus}
        </if>
        <!-- 此处追加所有业务查询条件 -->
    </sql>

    <!-- 分页查询 -->
    <select id="queryBizDetail" resultType="com.xxx.dto.BizDataDTO">
        <include refid="query_biz_detail_common"/>
        <!-- 禁止手写limit,由MyBatis‑Plus分页拦截器自动拼接 -->
    </select>

    <!-- 全量查询,不分页 -->
    <select id="queryBizDetailAll" resultType="com.xxx.dto.BizDataDTO">
        <include refid="query_biz_detail_common"/>
    </select>

</mapper>

3、Service 业务层逻辑

对外返回统一 IPage,内部根据业务标识分支调用不同 Mapper 方法。

scss 复制代码
@Service
public class BizDataService {

    @Autowired
    private BizDataMapper bizDataMapper;

    public IPage<BizDataDTO> getBizDetailList(BizQueryDTO reqDto, boolean needPage) {
        IPage<BizDataDTO> result;
        if (needPage) {
            // 分页场景
            Page<BizDataDTO> page = new Page<>(reqDto.getPageNo(), reqDto.getPageSize());
            result = bizDataMapper.queryBizDetail(page, reqDto);
            handleBizAfterProcess(result.getRecords());
        } else {
            // 不分页场景,直接调用全量查询方法
            List<BizDataDTO> dataList = bizDataMapper.queryBizDetailAll(reqDto);
            handleBizAfterProcess(dataList);
            // 手动封装Page对象,保证出参统一
            result = new Page<>();
            result.setRecords(dataList);
            result.setTotal(dataList.size());
        }
        return result;
    }

    private void handleBizAfterProcess(List<BizDataDTO> list) {
        // 业务后置处理逻辑
    }
}

两种方案横向对比

方案 优点 缺点 适用场景
双 Mapper 方法 + SQL 片段复用(本文方案) 1. 不依赖分页拦截器状态,稳定性高; 2. 代码语义清晰,一眼区分分页 / 全量; 3. 不会出现 LIMIT 残留 bug; 4. 条件只维护一份,好维护 需要新增一个 Mapper 方法 生产环境首选,尤其框架封装较重的项目
new Page<>(0,-1)关闭分页 只写一套 Mapper 方法 强依赖拦截器,框架 / 版本变化容易失效;XML 手写 limit 直接失效 测试、简单 demo,不建议生产使用

生产环境关键注意点

  1. 分页 XML 中禁止手写 LIMIT ,分页 SQL 交给 MyBatis‑Plus 拦截器自动拼接;一旦手写 LIMIT,size=-1完全失效。
  2. 不要试图对 Mapper 接口做方法重载,MyBatis 不支持同名不同参数绑定同一个 XML id。
  3. 全量查询返回 List 后手动组装Page,对外接口保持返回IPage,不需要修改上层接口定义。
  4. 使用<sql>+<include>复用 SQL,杜绝复制粘贴大段 SQL,避免后续改查询条件漏改一处。
  5. 分页拦截器必须正确配置,保证分页模式正常工作。

总结

Page(size=-1)是一个语法糖,适合简单 Demo;真实业务系统,框架环境复杂,拦截器有可能被干扰,不建议依赖这种隐式特性。

通过两个 Mapper 方法 + MyBatis SQL 片段复用,只是少量代码成本,换来更高的稳定性、可读性与可维护性,规避线上诡异分页 bug,是更稳妥的工程实践。

掘金标签:#MyBatis-Plus #Java后端 #分页 #实战踩坑 #SpringBoot

相关推荐
Java内核笔记1 小时前
BeanRegistrar:Spring Boot 4 最被低估的新特性,彻底改变 Bean 注册方式
java·后端
还是鼠鼠1 小时前
【Spring AI智能体实战 02】Spring Boot 3.4整合Spring AI:项目创建与依赖配置
java·人工智能·spring boot·maven·spring ai
搬砖小匠1 小时前
SpringBoot 通过自定义注解 + AOP 实现数据字典自动翻译(通用方案)
java·后端
今天AI了吗1 小时前
【AI智能体】Hermes Agent 从部署到项目实战操作详解
java·人工智能·python·milvus
青山木2 小时前
Hot 100 --- 搜索二维矩阵
java·算法·leetcode·矩阵
CoderYanger2 小时前
Java EE:9.JVM(课件内容-上篇)
java·jvm·程序人生·面试·职场和发展·java-ee·程序员创富
xiaoqiMikko2 小时前
Spring Cloud Config 这个 CVE 的补丁,Maven Central 上根本没有
java·spring boot
残月心殇 请珍惜枸2 小时前
.NET陷阱之五:奇怪的OutOfMemoryException——大对象堆引起的问题与对策
java·jvm·.net
用户3126874877202 小时前
Spring Boot 配置体系到底怎么加载的?从 application.yml 到 @Conditional 的全链路拆解
java·spring boot