前言
业务开发中经常遇到一种场景:同一个查询逻辑,根据前端传参动态选择分页查询 / 查询全部数据 。很多同学第一想法:复用同一套 Mapper 接口,传入Page(pageNum,-1)实现不分页,一行代码搞定。
但实际项目落地时,经常出现各种诡异问题:设置 size=-1,打印 SQL 依然出现LIMIT;IPage 的 records 为空;拦截器不生效等。尤其在二次封装框架环境下,分页拦截器加载异常会进一步放大问题。
本文基于真实业务实战,给出一套稳定、可维护的生产级实现方案。
环境:SpringBoot + MyBatis‑Plus,自定义 XML 手写 SQL,非 MP 自带 Wrapper 查询。
业务场景
业务需求:查询业务明细,支持两种模式
- 分页模式:按照页码、页大小返回 IPage 对象,用于前端表格分页渲染;
- 全量模式:不需要分页,查询全部数据,做后续业务计算处理,接口返回结构依然统一返回
IPage。
最初想法:只写一个 Mapper 方法,通过传入new Page<>(0,-1),依靠 MyBatis‑Plus 内置特性关闭分页,一份 Mapper、一份 XML 搞定两种逻辑。
遇到的现实问题
Page(0,-1)不分页特性强依赖 MyBatis‑Plus 分页拦截器正常注册、生效。- 如果项目框架存在拦截器重复 / 缺失、版本差异、XML 手写 LIMIT,
size=-1会直接失效,SQL 依旧拼接 LIMIT,只能查到部分数据。 - Mapper 接口直接传
null给 IPage 参数,会触发空指针异常。 - MyBatis 本身不支持 Java 接口重载绑定同一个 XML 的
id,同名不同参数重载会抛出 Mapper 绑定异常。
核心结论:生产环境不建议依赖 Page(size=-1) 做全量查询,它属于便捷语法,环境稍有变化就会出线上 bug。
实战最优方案:双 Mapper 方法 + SQL 片段复用
核心思路:
- Mapper 层提供两个不同名称的方法:一个带 IPage 用于分页;一个不带 IPage 直接返回 List,用于查询全部;
- XML 使用
<sql>抽取公共查询片段,两个<select>通过<include>复用同一套查询逻辑,SQL 只维护一份,避免复制粘贴带来的维护灾难; - 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,不建议生产使用 |
生产环境关键注意点
- 分页 XML 中禁止手写 LIMIT ,分页 SQL 交给 MyBatis‑Plus 拦截器自动拼接;一旦手写 LIMIT,
size=-1完全失效。 - 不要试图对 Mapper 接口做方法重载,MyBatis 不支持同名不同参数绑定同一个 XML id。
- 全量查询返回 List 后手动组装
Page,对外接口保持返回IPage,不需要修改上层接口定义。 - 使用
<sql>+<include>复用 SQL,杜绝复制粘贴大段 SQL,避免后续改查询条件漏改一处。 - 分页拦截器必须正确配置,保证分页模式正常工作。
总结
Page(size=-1)是一个语法糖,适合简单 Demo;真实业务系统,框架环境复杂,拦截器有可能被干扰,不建议依赖这种隐式特性。
通过两个 Mapper 方法 + MyBatis SQL 片段复用,只是少量代码成本,换来更高的稳定性、可读性与可维护性,规避线上诡异分页 bug,是更稳妥的工程实践。
掘金标签:
#MyBatis-Plus#Java后端#分页#实战踩坑#SpringBoot