记一次mybatis与pagehelper分页查询bug

118 阅读5分钟

背景

本文写自2024.3.22。

自三月入职以来,也是于今日完成了接手的第一类需求,开发一个页面的接口,具体就是包含常规的crud,以及使用feign和httpclient对于其他微服务与公司基础架构的调用。

虽然自己有自己的接口风格,不过初来咋到肯定是要吸收一下前辈们的《生产级》代码的。

在mentor对我的代码进行code review时也学到了许多经验,这大概也是实习的意义了。

不扯远了步入正题。

公司的分页查询是使用Mybatis + PageHelper实现的,大概也是大家常见的一些代码。

经典的就是:

 PageHelper.startPage(pageNum,pageSize);

我也是这么写的,但是在实际调用的情况下发现,pageSize明明写的10,数据库中的数据也是有上千条的,但是返回的结果却是有时候小于10,有时候等于10。

使用PageHelper比较多的大佬应该知道,这是因为使用了多表联合查询,再由Mybatis聚合成对象,导致分页并没有实现在想要实现的地方。

问题描述与复现

问题描述:关于mybatis+pagehelper,分页数据返回与预期不一致问题。

接下来我来实现一个demo来复现这个问题。

定义一个School类:

@Data
public class School {
    private Long id;
    private String name;
    private List<Student> studentList;
}

再定义一个Student类:

@Data
public class Student {
    private Long id;
    private String name;
    private Long schoolId;
}

可以很明显的分辨出来,School和Student是一对多的关系。

再定义一个基础的SchoolDao和SchoolDao.xml文件,这个就不做演示了。

在SchoolDao.xml文件中写ResultMap,如下:

    <resultMap id="SchoolStudentMap" type="org.example.temp.School">
        <id column="id" property="id"></id>
        <result column="name" property="name"></result>
        <collection property="studentList" resultMap="Student"></collection>
    </resultMap>

    <resultMap id="Student" type="org.example.temp.Student">
        <id column="s_id" property="id"></id>
        <result column="s_name" property="name" javaType="java.lang.String" jdbcType="VARCHAR"></result>
        <result column="s_school_id" property="schoolId"></result>
    </resultMap>

如果您不知道为什么要用id标签或collection您还不太会用,建议您先学习Mybatis的使用后再看此文章。

现在定义一个需求,要新增一个学校的管理页,学校管理页要对学校和学校的学生进行管理,要求做一个分页查询的接口,同时返回该学校和学校的学生。

好先补上简单的SQL语句:包含建表语句和数据插入语句。

student表:

-- Table structure for student
-- ----------------------------
DROP TABLE IF EXISTS `student`;
CREATE TABLE `student`  (
  `id` bigint NOT NULL,
  `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `school_id` bigint NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of student
-- ----------------------------
INSERT INTO `student` VALUES (1, 'gyf', 1);
INSERT INTO `student` VALUES (2, 'gyy', 2);
INSERT INTO `student` VALUES (3, 'gyj', 1);

school表:

-- ----------------------------
-- Table structure for school
-- ----------------------------
DROP TABLE IF EXISTS `school`;
CREATE TABLE `school`  (
  `id` bigint NOT NULL,
  `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of school
-- ----------------------------
INSERT INTO `school` VALUES (1, '985大学');
INSERT INTO `school` VALUES (2, '211大学');
INSERT INTO `school` VALUES (3, '本科');
INSERT INTO `school` VALUES (4, '普通一本');
INSERT INTO `school` VALUES (5, '二本');
INSERT INTO `school` VALUES (6, '公办三本');
INSERT INTO `school` VALUES (7, '民办三本');
INSERT INTO `school` VALUES (8, '大专');
INSERT INTO `school` VALUES (9, '中专');
INSERT INTO `school` VALUES (10, '初中');

根据需求写一个简单的join查询语句。

        SELECT
        school.id as "id",
        school.name as "name",
        student.id as "s_id",
        student.name as "s_name",
        student.school_id as "s_school_id"
        FROM school school
        left join student student
        on school.id = student.school_id
        limit 0,10

不知道为什么用left join的同学建议去学一学sql基础查询语句。

这样是能查出来10条数据的。

我们把sql语句用Mybatis的方式写入。

代码示例:

    <select id="selectSchoolList" resultMap="SchoolStudentMap">
        SELECT
        school.id as "id",
        school.name as "name",
        student.id as "s_id",
        student.name as "s_name",
        student.school_id as "s_school_id"
        FROM school school
        left join student student
        on school.id = student.school_id
        limit 0,10
    </select>

然后在test文件中调用并输出结果:

    @Resource
    private SchoolDao schoolDao;
    @Test
    public void JwTest1() {
        List<School> schools = schoolDao.selectSchoolList();
        System.out.println(schools.size());
    }

结果如下:

[School(id=1, name=985大学, studentList=[Student(id=3, name=gyj, schoolId=1), Student(id=1, name=gyf, schoolId=1)]), School(id=2, name=211大学, studentList=[Student(id=2, name=gyy, schoolId=2)]), School(id=3, name=本科, studentList=[]), School(id=4, name=普通一本, studentList=[]), School(id=5, name=二本, studentList=[]), School(id=6, name=公办三本, studentList=[]), School(id=7, name=民办三本, studentList=[]), School(id=8, name=大专, studentList=[]), School(id=9, name=中专, studentList=[]), School(id=10, name=初中, studentList=[])]

很简单能发现,10条数据的list对象长度仅为9个School对象了,因为有两行数据被合并为一行了。

这种合并操作是由Mybatis自动为我们进行的,在后文我会详细讲解这种机制。

这也是为什么使用PageHelper进行分页时(自己手写limit也是一样),会出现返回数据个数小于pageSize个数的问题。

解决思路

原因很简单,从前面的sql语句就能看到。

如何避免:

  1. 分析需求,要的分页逻辑其实是一张表,而不是多表联合后的查询数据。
  2. 因此可以使用内查询,在内查询中进行分页获取,再外查询in来查找。
  3. 但是pagehelper的自动插入的语句没办法插入到内查询,而是默认在最外层。
  4. 那也可以自己去写分页语句,但是这样pageinfo的数据就没了,那还是比较麻烦,也不符合公司规范。
  5. 所以放弃内查询用法,先进行一次分页单表查询,再进行多表联合查询。
  6. 再把查询结果的单表pageinfo转换为多表pageinfo即可。
  7. 虽然这样的那个set语句就太长了。。
  8. 但是问题还是完美解决了

问题解读+源码探索

问题解决了,不过也让我再回顾来看,在解决问题的过程中也遇到了几个比较有意思的点。

  1. mybatis查询返回后对象注入的源码
  2. pagehelper的pageinfo是怎么构造怎么来的,具体的运行流程,源码级

留个悬念在此吧。。待我学习mybatis源码后再补上。