解决 MyBatis 使用 JOIN 查询导致分页总数统计错误的问题

解决 MyBatis 使用 JOIN 查询导致分页总数统计错误的问题(优化后)

问题背景

在开发中,我们经常需要通过 JOIN 查询实现一对多关系的数据聚合(例如查询一个保养方案关联的多个设备或项目)。但在使用 MyBatis 分页插件 PageHelper 时,发现分页总数(total)与实际主表记录数不符。例如:

  • 主表 maintenance_plan3 条记录
  • 关联表 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");

优点

  • 代码简洁,一行配置即可
  • 自动同步查询条件

发现问题

  • 虽然解决了总数统计问题,但会导致分页数据被截断
  • 当主表记录在关联表中有多条匹配时,可能丢失部分关联数据

优化方案:分离主表计数与关联查询

最终采用分离主表计数与关联查询的方式:

  1. 先单独查询主表记录数
  2. 再执行带 JOIN 的分页查询
  3. 手动设置分页总数
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复杂关联查询,需要保证数据完整性

注意事项

  1. 性能优化
    • 为主表的查询条件字段添加索引
    • 对于大数据量表,考虑缓存分页总数
  2. 一致性保证
    • 确保计数查询和主查询的条件完全一致
    • 对于动态条件,使用相同的判断逻辑
  3. 分页插件版本
    • 方案适用于 PageHelper 5.x 版本
    • 低版本可能需要调整实现方式

总结

通过分离主表计数与关联查询的方案,我们既解决了分页总数统计错误的问题,又保证了关联数据的完整性。这种方案虽然需要多写一个计数查询,但在数据准确性和系统稳定性方面提供了更好的保障。

对于不同的业务场景,开发者可以根据实际需求选择最合适的方案。在需要精确统计且关联数据重要的场景下,推荐使用分离查询的方案;在简单关联且对关联数据完整性要求不高的场景下,可以使用 setCountColumn 简化实现。

@感谢提出问题的小伙伴

### MyBatis 中实现一对多关系查询并带分页功能 #### 使用 `collection` 标签进行一对多查询 为了在 MyBatis 中实现一对多的关系查询,通常可以使用 `<resultMap>` 配合 `<association>` 或者 `<collection>` 来映射复杂的结果集。对于一对多的情况,主要采用 `<collection>` 标签来表示子项集合。 ```xml <resultMap id="ServerWithUsersResultMap" type="com.example.Server"> <id property="id" column="server_id"/> <!-- 其他字段 --> <collection property="users" ofType="com.example.User"> <id property="userId" column="user_id"/> <!-- 用户其他属性 --> </collection> </resultMap> <select id="selectServersWithPaging" resultMap="ServerWithUsersResultMap"> SELECT * FROM server s LEFT JOIN user u ON s.id = u.server_id LIMIT #{offset},#{limit} </select> ``` 上述代码片段定义了一个名为 `ServerWithUsersResultMap` 的结果映射,用于将服务器及其关联用户的记录组合成 Java 对象结构[^1]。 #### 解决分页带来的总数统计问题 当引入分页机制时,可能会遇到一个问题:即返回的数据条目数可能超过预期的总记录数。这是因为默认情况下,MyBatis 可能会对每一条父级记录都执行一次子表的全量查询,从而导致重复计算的问题。为了避免这种情况的发生,在设计 SQL 查询语句时应确保只针对主表应用分页逻辑,并且可以通过额外的一次单独查询获取真实的总记录数目。 一种常见的解决办法是在 XML 映射文件中分别编写两个独立的操作: - **查询列表**:仅对主表施加分页限制; - **查询总数**:不受任何行数限制地统计符合条件的所有记录的数量。 ```sql <!-- 获取分页后的服务端数据 --> <select id="getServersByPage" parameterType="map" resultType="java.util.Map"> SELECT t.*, COUNT(u.user_id) OVER() AS totalElements FROM ( SELECT * FROM server ORDER BY id ASC LIMIT #{pageSize} OFFSET #{startIndex} ) t LEFT JOIN user u ON t.id = u.server_id; </select> <!-- 单独查询总的服务器数量 --> <select id="countAllServers" resultType="int"> SELECT COUNT(*) FROM server WHERE 1=1 ${dynamicConditions}; </select> ``` 这段SQL不仅实现了基于主表的服务端信息分页显示,还利用窗口函数 `COUNT()` 计算了整个结果集中所含有的元素个数,以便前端展示正确的总计数值[^3]。 #### 利用插件简化分页操作 考虑到手写复杂的分页逻辑容易出错以及维护成本较高,推荐借助成熟的第三方库如 PageHelper 插件来进行更高效的分页管理。该工具可以在不影响原有业务逻辑的前提下轻松集成到项目当中,极大地方便开发者快速构建高效稳定的分页方案。 安装依赖后只需简单配置即可生效,具体可参阅官方文档说明[^4]。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值