最近在重构一个教师管理系统时,我遇到了一个诡异的问题。系统需要展示教师列表,每个教师关联多个学生记录。使用Mybatis-Plus进行分页查询时,前端明明请求每页显示2条教师记录,返回的数据却总是出现重复教师信息。更奇怪的是,分页总数显示正确(4位教师应该分2页),但实际数据却像"幽灵"一样重复出现。
这个问题在LEFT JOIN多表查询时尤为明显。比如教师表有4条记录,每个教师关联2个学生,执行分页查询时会出现:
通过日志分析生成的SQL,发现Mybatis-Plus的分页插件在处理多表关联时,COUNT语句和实际查询语句存在逻辑断层。COUNT语句正确地统计了教师表记录数(SELECT COUNT(*) FROM teacher),但实际查询语句却是SELECT * FROM teacher LEFT JOIN student LIMIT 2,这就导致返回的是关联后的笛卡尔积结果。
Mybatis-Plus的PaginationInnerInterceptor通过拦截Executor实现分页。其核心逻辑是:
java复制// 典型配置示例
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
这种机制在单表查询时表现良好,但在多表场景下会产生"计数准确但数据错乱"的现象。我曾在一个电商项目中遇到类似问题:统计商品数量正确,但分页返回的商品列表却出现重复。
PageHelper采用ThreadLocal绑定分页参数,其特点包括:
xml复制<!-- PageHelper配置 -->
<plugin interceptor="com.github.pagehelper.PageInterceptor">
<property name="helperDialect" value="mysql"/>
</plugin>
实测发现,同样的教师-学生关联查询,PageHelper返回的total会是8(4教师×2学生),这显然不符合业务预期。去年帮一个朋友排查库存管理系统分页问题时,就遇到过PageHelper因多表关联导致分页总数膨胀10倍的情况。
当使用PageHelper进行多表LEFT JOIN查询时,生成的COUNT语句会包含关联条件:
sql复制SELECT COUNT(*) FROM teacher t LEFT JOIN student s ON t.class=s.class
这会导致统计的是关联后的记录总数(8条),而非主表记录数(4条教师)。在物流系统的运单-货物关联查询中,这个错误会让分页控件显示100页,实际数据只有20单。
Mybatis-Plus的分页是基于主表记录数计算偏移量。假设:
sql复制SELECT * FROM teacher t LEFT JOIN student s LIMIT 2,2
但由于LEFT JOIN产生多条关联记录,实际获取的可能是同一个教师的多个学生记录,而非期望的不同教师。
最棘手的"幽灵数据"现象表现为:
这通常发生在Mybatis-Plus的级联查询场景。我曾在CRM系统中遇到:查询第2页客户信息时,系统重复返回第1页的客户,但总数显示正常。原因是级联查询的分页在主表进行,而数据组装时又通过子查询获取关联数据。
xml复制<select id="getTeachers" resultMap="teacherMap">
SELECT t.* FROM teacher t
LEFT JOIN student s ON t.class=s.class
<where>...</where>
GROUP BY t.id <!-- 按教师ID分组 -->
LIMIT #{page},#{size}
</select>
注意事项:
在政务系统项目中,这个方案成功解决了200万+数据量的分页问题,查询耗时从8s降至200ms。
sql复制SELECT t.*,s.* FROM
(SELECT * FROM teacher LIMIT 0,10) t
LEFT JOIN student s ON t.class=s.class
优势:
java复制// 先获取全部关联数据
List<TeacherVO> list = teacherMapper.getAll();
// 内存分页
List<TeacherVO> pageList = list.stream()
.skip((page-1)*size).limit(size)
.collect(Collectors.toList());
适用场景:
Mybatis-Plus支持优化COUNT查询:
java复制Page<TeacherVO> page = new Page<>(1, 10);
page.setOptimizeCountSql(false);
page.setSearchCount(true);
可以在XML中定义专用COUNT语句:
xml复制<select id="selectTeacherPage" resultMap="...">
SELECT t.* FROM teacher t LEFT JOIN...
</select>
<select id="selectTeacherCount" resultType="long">
SELECT COUNT(DISTINCT t.id) FROM teacher t
</select>
根据查询条件自动选择分页策略:
java复制public Page<TeacherVO> getPage(TeacherQuery query) {
if (query.hasStudentCondition()) {
// 多表条件查询使用子查询分页
return teacherMapper.selectWithSubPage(query);
} else {
// 单表查询使用常规分页
return teacherMapper.selectSimplePage(query);
}
}
添加分页性能监控点:
java复制long start = System.currentTimeMillis();
page = teacherMapper.selectPage(page, query);
long cost = System.currentTimeMillis() - start;
if (cost > 1000) {
log.warn("慢分页查询: {}, 参数: {}",
page.getRecords().size(), query);
}