记一次糟糕的 MyBatis-Plus 查询设计:那个“非得查 COUNT”的分页坑

“我只是想查个列表,你为什么要偷偷多执行一条 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:

  1. SELECT COUNT(*) FROM ...
  2. SELECT * FROM ... LIMIT offset, size

🤯 但问题是:很多时候,我只需要一个列表!

比如:

  • 无限滚动加载【游标】(前端自己拼接列表)
  • 数据量不大,前端直接渲染完事
  • 列表为空时,COUNT 根本没意义

👉 多出来的 COUNT 查询,不仅是性能浪费,更是设计上的“越界”!

🚫 违背迪米特法则(Law of Demeter)

“一个对象应该对其他对象保持最少的了解。”

我们想要的只是一个 列表数据,但 MyBatis-Plus 却“好心”地把 总数、总页数、当前页 全塞给我。

这就像:

你去便利店买瓶水,店员却坚持要给你开一张“本月消费统计报表”。

返回了不该返回的信息,耦合了不必要的逻辑。


✅ 我们的解决方案:让 list 只返回 list

✅ 目标明确:

list() 方法只查数据,不查 COUNTcount() 方法单独提供,按需调用。

✅ 改造后的 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对单表进行操作,且减少使用分页插件
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值