背景
本文写自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语句就能看到。
如何避免:
- 分析需求,要的分页逻辑其实是一张表,而不是多表联合后的查询数据。
- 因此可以使用内查询,在内查询中进行分页获取,再外查询in来查找。
- 但是pagehelper的自动插入的语句没办法插入到内查询,而是默认在最外层。
- 那也可以自己去写分页语句,但是这样pageinfo的数据就没了,那还是比较麻烦,也不符合公司规范。
- 所以放弃内查询用法,先进行一次分页单表查询,再进行多表联合查询。
- 再把查询结果的单表pageinfo转换为多表pageinfo即可。
- 虽然这样的那个set语句就太长了。。
- 但是问题还是完美解决了
问题解读+源码探索
问题解决了,不过也让我再回顾来看,在解决问题的过程中也遇到了几个比较有意思的点。
- mybatis查询返回后对象注入的源码
- pagehelper的pageinfo是怎么构造怎么来的,具体的运行流程,源码级
留个悬念在此吧。。待我学习mybatis源码后再补上。