“我只是想查个列表,你为什么要偷偷多执行一条
COUNT
?”
—— 当你在用 MyBatis-Plus 做分页时,有没有想过:我真的需要总数吗?
记录一次在使用 MyBatis-Plus 时,被“默认分页必须查 COUNT
”这一设计逼到重构的经历。
问题起源:Map<String, Object>
参数,前端看不懂!
先说个小问题【不合理设计】:
@RequestMapping("/list")
public R list(@RequestParam Map<String, Object> params) {
PageUtils page = pmsAttrGroupService.queryPage(params);
return R.ok().put("page", page);
}
❌问题:
- 前端看 Swagger 文档:“这 params 到底要传啥?”
- 后端解析:
params.get("categoryId")
,类型靠猜,容易出错。 - 阅读体验极差,扩展性为零。
这不是 API,这是“黑盒通信”。
✅ 优化第一步:用 Query
对象替代 Map
我们为每个查询建立独立的 Query
类,结构清晰、类型安全:
@RequestMapping("/list")
public R list(@RequestBody PsmAttrGroupQuery query) {
return R.ok().put("page", pmsAttrGroupService.list(query));
}
🧱 Query 模型设计
@Data
public class PsmAttrGroupQuery extends BaseQueryModel {
private Long categoryId; // 所属分类id
}
@Data
public class BaseQueryModel {
private Integer page = 1;
private Integer limit = 10;
private String orderBy = "asc";
private String keyword = "";
}
✅ 效果立竿见影:
- Swagger 自动生成字段说明,前端终于“看得懂”了。
- 类型安全,编译期就能发现问题。
- 模块化管理,
query/
目录一目了然。
核心痛点:MyBatis-Plus 的“强制 COUNT”是个糟糕的设计!
这才是本文的重点,也是我们忍了很久的坑。
❌ 原始分页代码
@Override
public PageUtils queryPage(Map<String, Object> params) {
IPage<PmsAttrGroupEntity> page = this.page(
new Query<PmsAttrGroupEntity>().getPage(params),
new QueryWrapper<PmsAttrGroupEntity>()
);
return new PageUtils(page);
}
🔍 问题本质
只要你调用 this.page()
,MyBatis-Plus 就会默认执行两条 SQL:
SELECT COUNT(*) FROM ...
SELECT * FROM ... LIMIT offset, size
🤯 但问题是:很多时候,我只需要一个列表!
比如:
- 无限滚动加载【游标】(前端自己拼接列表)
- 数据量不大,前端直接渲染完事
- 列表为空时,
COUNT
根本没意义
👉 多出来的 COUNT
查询,不仅是性能浪费,更是设计上的“越界”!
🚫 违背迪米特法则(Law of Demeter)
“一个对象应该对其他对象保持最少的了解。”
我们想要的只是一个 列表数据,但 MyBatis-Plus 却“好心”地把 总数、总页数、当前页 全塞给我。
这就像:
你去便利店买瓶水,店员却坚持要给你开一张“本月消费统计报表”。
❌ 返回了不该返回的信息,耦合了不必要的逻辑。
✅ 我们的解决方案:让 list
只返回 list
!
✅ 目标明确:
list()
方法只查数据,不查COUNT
;count()
方法单独提供,按需调用。
✅ 改造后的 Service
@Override
public PageResult<PmsAttrGroupEntity> list(PsmAttrGroupQuery query) {
Page<PmsAttrGroupEntity> page = new Page<>(query.getPage(), query.getLimit());
QueryWrapper<PmsAttrGroupEntity> wrapper = new QueryWrapper<>();
wrapper.eq(query.getCategoryId() != null, "catelog_id", query.getCategoryId());
wrapper.like(StringUtils.isNotBlank(query.getKeyword()), "attr_group_name", query.getKeyword());
// ⚠️ 注意:这里仍然用了 page(),所以 COUNT 还是执行了
// 但由于公司架构限制,无法彻底移除分页插件,只能“适应”
Page<PmsAttrGroupEntity> result = this.page(page, wrapper);
return PageUtils.convertToPage(result, PmsAttrGroupEntity.class);
}
📌 现实很骨感:由于公司整体技术架构依赖 MyBatis-Plus 的分页插件,我们无法完全去掉
COUNT
,只能通过设计上“弱化”它的影响。
🛠️ 工具类:PageUtils.convertToPage
,让返回更安全
为了让调用方拿到干净的 List<T>
,我们封装了一个通用转换方法:
public static <T> PageResult<T> convertToPage(Page<?> page, Class<T> targetType) {
PageResult<T> pageResult = new PageResult<>();
pageResult.setTotalCount(page.getTotal());
pageResult.setPageSize(page.getSize());
pageResult.setTotalPage(page.getPages());
pageResult.setCurrPage(page.getCurrent());
List<?> recordList = page.getRecords();
if (recordList == null || recordList.isEmpty()) {
pageResult.setList(new ArrayList<>());
} else {
if (targetType != null) {
pageResult.setList(Convert.toList(targetType, recordList));
} else {
@SuppressWarnings("unchecked")
List<T> sameTypeList = (List<T>) recordList;
pageResult.setList(sameTypeList);
}
}
return pageResult;
}
✅ PageResult<T>
返回结构
@Data
public class PageResult<T> {
private Long totalCount;
private Long pageSize;
private Long totalPage;
private Long currPage;
private List<T> list; // 核心:这才是前端真正需要的
}
🎯 理想中的正确做法(无法实现,但值得追求)
如果架构允许,最合理的做法是:
// 只查列表,不查 COUNT
List<T> list(Query query);
// 单独查总数,按需调用
long count(Query query);
✅ 这样才能真正做到:
- 按需加载,避免性能浪费
- 职责分离,符合单一职责原则
- 不违背迪米特法则
适应框架,但不盲从设计。
📝 总结:MyBatis-Plus 的“便利”背后,是设计的妥协
问题 | 现象 | 我们的应对 |
---|---|---|
Map 参数混乱 | 前端看不懂,后端难维护 | ✅ 引入 Query 对象 |
强制 COUNT 查询 | 多余 I/O,违背设计原则 | ⚠️ 无法避免,但逻辑隔离 |
返回耦合 | 列表 + 总数强绑定 | ✅ 用 PageResult 封装,明确语义 |
“MyBatis-Plus 让我们写分页变得简单,框架的默认行为 ≠ 最佳实践
- 性能优化,从减少不必要的查询开始
- 设计原则(如迪米特法则)在高并发场景下,比你想象的重要
- 如果可以的话,只用mybatisPlus对单表进行操作,且减少使用分页插件