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

相关推荐
10mAh15 分钟前
【Java】HashMap 的 put 到底做了什么?——冲突、扩容与树化实测
java·开发语言·hash
我命由我1234533 分钟前
人脸识别 - 勒克斯(光线照射到物体表面上的明亮程度)
android·java·学习·java-ee·人脸识别·学习方法·android runtime
TechLee1 小时前
跨语言加解密总对不上?这个纯 Go 神库让 AES/RSA 与 PHP、Java 100% 互通
java·后端·算法
LINgZone21 小时前
系统架构与分支规范
java·数据库·架构
AC赳赳老秦1 小时前
企业级合规审计体系:用 OpenClaw 落地采集全链路留痕,自动生成合规审计报告
java·python·django·beautifulsoup·php·deepseek·openclaw
吃杠碰小鸡2 小时前
VsCode中开发Java项目
java·vscode
宠友信息3 小时前
内容社区源码开发实践解析,用Spring Boot打造1:1仿小红书源码平台
java·spring boot·redis·websocket·mysql·spring·uni-app
行百里er3 小时前
加个依赖就生效?一行搞定 Spring Boot Starter 自动装配
java·后端·监控
月华路3 小时前
G1 新生代对象晋升老年代:实现机制与 GC 日志
java·jvm·算法