解决 MyBatis 使用 JOIN 查询导致分页总数统计错误的问题(优化后)
问题背景
在开发中,我们经常需要通过 JOIN
查询实现一对多关系的数据聚合(例如查询一个保养方案关联的多个设备或项目)。但在使用 MyBatis 分页插件 PageHelper 时,发现分页总数(total
)与实际主表记录数不符。例如:
- 主表
maintenance_plan
有 3 条记录 - 关联表
plan_device_map
(1条) 和plan_item_map
(1条)通过LEFT JOIN
查询后,返回 5 条结果 - 分页总数错误:PageHelper 默认统计所有关联后的行数(
total=5
),而实际需要的是主表的记录数(total=3
)
问题分析
原因
- 分页插件默认行为:PageHelper 会生成
COUNT(*)
查询,统计所有匹配的行数。当存在JOIN
时,主表记录会被关联表的行数放大 - 错误结果:分页总数基于关联后的行数,而非主表的实际记录数
影响
- 分页控件显示错误的总页数
- 用户无法正确翻页,导致数据展示混乱
解决方案演进
初始方案:使用 setCountColumn 方法
最初尝试通过 setCountColumn
方法指定按主表 ID 去重统计:
PageHelper.startPage(pageNum, pageSize).setCountColumn("DISTINCT mp.id");
优点:
- 代码简洁,一行配置即可
- 自动同步查询条件
发现问题:
- 虽然解决了总数统计问题,但会导致分页数据被截断
- 当主表记录在关联表中有多条匹配时,可能丢失部分关联数据
优化方案:分离主表计数与关联查询
最终采用分离主表计数与关联查询的方式:
- 先单独查询主表记录数
- 再执行带 JOIN 的分页查询
- 手动设置分页总数
public PageInfo<MaintenancePlanVO> find(MaintenancePlan maintenancePlan, int pageNum, int pageSize) {
// 1. 单独查询主表记录数
long total = maintenancePlanMapper.count(maintenancePlan);
// 2. 执行带JOIN的分页查询
PageHelper.startPage(pageNum, pageSize);
List<MaintenancePlanVO> maintenancePlanVOList = maintenancePlanMapper.find(maintenancePlan);
// 3. 手动设置分页总数
PageInfo<MaintenancePlanVO> pageInfo = new PageInfo<>(maintenancePlanVOList);
pageInfo.setTotal(total);
return pageInfo;
}
完整实现
Controller 层
@GetMapping("/find")
public AjaxResult find(MaintenancePlan maintenancePlan,
@RequestParam(defaultValue = "1") int pageNum,
@RequestParam(defaultValue = "10") int pageSize) {
PageInfo<MaintenancePlanVO> pageInfo = maintenancePlanService.find(maintenancePlan, pageNum, pageSize);
return success(pageInfo);
}
Mapper 层
主表计数方法
int count(MaintenancePlan maintenancePlan);
<select id="count" resultType="int">
SELECT COUNT(*) FROM maintenance_plan
<where>
<if test="planName != null and planName != ''">
AND plan_name LIKE CONCAT('%', #{planName}, '%')
</if>
</where>
</select>
主查询方法(不变)
<select id="find" resultMap="MaintenancePlanVOResultMap">
SELECT
mp.*,
pd.id as device_id,
pd.device_uid,
pi.id as item_id,
pi.item_id,
pi.order_index
FROM maintenance_plan mp
LEFT JOIN plan_device_map pd ON mp.id = pd.plan_id
LEFT JOIN plan_item_map pi ON mp.id = pi.plan_id
<where>
<if test="planName != null and planName != ''">
AND mp.plan_name LIKE CONCAT('%', #{planName}, '%')
</if>
</where>
</select>
结果映射(不变)
<resultMap id="MaintenancePlanVOResultMap" type="MaintenancePlanVO">
<id property="id" column="id"/>
<!-- 主表字段映射 -->
<!-- 关联设备信息 -->
<collection property="planDeviceMapList" ofType="PlanDeviceMap">
<result property="id" column="device_id"/>
<result property="planId" column="plan_id"/>
<result property="deviceUid" column="device_uid"/>
</collection>
<!-- 关联项目信息 -->
<collection property="planItemMapList" ofType="PlanItemMap">
<result property="id" column="item_id"/>
<result property="planId" column="plan_id"/>
<result property="itemId" column="item_id"/>
<result property="orderIndex" column="order_index"/>
</collection>
</resultMap>
方案对比
方案 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
setCountColumn | 实现简单,自动同步查询条件 | 可能导致分页数据截断 | 简单关联查询,不关心关联数据完整性 |
分离计数查询 | 数据准确,不会丢失关联数据 | 需要额外编写计数SQL | 复杂关联查询,需要保证数据完整性 |
注意事项
- 性能优化
- 为主表的查询条件字段添加索引
- 对于大数据量表,考虑缓存分页总数
- 一致性保证
- 确保计数查询和主查询的条件完全一致
- 对于动态条件,使用相同的判断逻辑
- 分页插件版本
- 方案适用于 PageHelper 5.x 版本
- 低版本可能需要调整实现方式
总结
通过分离主表计数与关联查询的方案,我们既解决了分页总数统计错误的问题,又保证了关联数据的完整性。这种方案虽然需要多写一个计数查询,但在数据准确性和系统稳定性方面提供了更好的保障。
对于不同的业务场景,开发者可以根据实际需求选择最合适的方案。在需要精确统计且关联数据重要的场景下,推荐使用分离查询的方案;在简单关联且对关联数据完整性要求不高的场景下,可以使用 setCountColumn
简化实现。
@感谢提出问题的小伙伴