PageHelper的坑与尽量优雅的填坑(总条数错误)
§1 坑
在某些场景,PageHelper无法获取正确的总数(total)。
§2 坑成因
PageHelper一般使用时使用的是 PageHelper.startPage(pageNum, pageSize)。其工作原理是拦截此方法后第一个查询,对其进行分页,并自动解析sql ,拼接出一个查询数量的sql并执行,最后将两个查询(一个分页一个总数)的结果封装为一个 Page<E> 对象,Page<E> 是 List<E>的子类,total信息是其中的一个字段。
若因为某些原因,此Page
§3 有人没有踩到坑
§3.1 可能是因为业务场景太简单了
在业务场景很简单的情况下,也许可能出现数据经过查询后,从 Mapper 一路路过了 Service 直接给到了 Controller 的情况。此时,PageHelper可以正常工作,并返回正确的结果。
但,需要此场景的人反思:
- 是不是把sql当service使了,从sql里一口气拿到了整个业务需要的数据,还完成了拼装和转换
- 是不是把pageHelper 下沉到了 service 层去使用了,代价是service层的接口必须返回 PageInfo
- 是不是忽略了前后数据模型的区别,进行了透传
§3.2 所谓的简单业务
另外,所谓的业务场景很简单其实是伪命题,按道理来说,绝大部分场景来说,都应该踩坑才是对的:
- 首先,从数据库中获取的对象,是对数据库数据的直接封装,俗称 Entity
- 接下来,前端的数据一般要求的是视图中的数据模型,俗称Vo/Model
- 这两种数据原则上是不允许进行混用的,就是前面说的透传(有的公司,甚至大厂使用所谓的DTO,这种实体只是看着好用而已,其实也是有坑的,在基础设施齐全的条件下,区分数据模型的方式更友好)
- 这意味着前后直接必然涉及到一次数据模型的转换,而转换时自然会丢失Page<E>的对象
上述分析并不是否认DTO的合理性,若因种种原因,项目开发时约定,数据模型设计从简、或使用DTO等方式是无可厚非的。只要不是业务较复杂、数据较多变的项目里,应为没有提前约定,且研发人员普遍因为种种原因(对没错,基本就是因为懒)导致后端模型一路扔出后端即可。
§4 解决方法
§4.1 分析和比较
目前常见(传抄)的处理方式是sql执行前开启分页,sql执行后直接包装页面,同时强调后面两句话之间不要插入其他业务
参考 §3.2 ,这其实并没有解决问题,依然存在模型转换时丢失的情况(某些场景下,因为接口的原因这个方式其实是行不通的,原因已经说过两次了)。此方法可以解决total不准确的问题,其实是通过强行将业务按在所谓的简单业务场景
同时反过来思考,这只是一个辅助我们分页用的小工具,为什么还要影响到我们怎么写业务?理想的辅助框架是:我们怎么写业务随便,符合某些接口上的约定即可,然后简单的进行一下框架的配置——它生效了!
另一种处理是手动补充对总数的查询,并最后将总数塞入PageInfo中。
这是一种虽然看着和写着都比较恶心(因为还得自己补一个取total的语句,还得自己调),但本质上远好于刚刚那个方法的处理方式,因为在本质上——不影响写业务!
但,依然存在问题:
- 首先,total语句自己写了,自己调用了
- 其次,从书写上不简洁(这是废话,相当于将PageHelper原本帮你省下的代码量又自己补回来一半)
- 最关键的是,这种方式下,已经没有必要使用 PageHelper 了,写个拦截器其实完全可以做到同样的事,代码还会有少幅度的减少
§4.2 思路
至少在mapper层,正确的total信息是会随着实际的 Page<E>对象进行回传的(想看到的话需要做一个强转),那就可以提取这个正确的信息,再想办法传给调用方。
一般,分页用于前端列表或对应功能的开放接口,通常带有其他查询参数,还经常遇到卡时间段的情况,当然分页的两个参数(页号、页容量)也是大宝天天见的状态。
因此,可以提供一种分页条件查询基类,里面是时间段字段、分页字段、操作人字段等,那么再把 total 作为一个字段扔进去也是勉强可以的。
同时,对于每一个复杂查询,提供对应的分页条件查询子类,当然,继承基类,这种类统一命名为QueryCondition。其数据由前端组织发送,SpringMvc(或其他框架)统一接收,广大研发同学不需要特别在意。
PS: QueryCondition 本身就是建议在项目除推广的约定的一部分,对于后端研发,约定优于编码
同时,此类型的参数最终是会直接传递给mapper层的(因为带着查询参数),因此只需要把total塞到这个对象中就可以把总数信息通知调用方。当然,有严谨的同学会说controller接收到的参数,和mapper入参的参数也不一定是同一个。可以写一个转换工具,在两方之间进行同步参数值,但最终尽量将总数塞回去,以方便统一处理,详见后文。
§4.3 具体方式
只有标红的才是日常研发需要做的,其他步骤属于项目的基本配置
§4.3.1 注解 @Count
§4.3.2 注解的使用
§4.3.3 处理注解的切面
GeneralPageCondition 和上文说的 QueryCondition是一回事
§4.3.4 切面的配置
§4.3.5 可以用注解限制限制一下实际数据类型
上图两个过期的方法是应为项目限制最终没用起来
§4.3.6 手动塞一下
PS: 这一步虽然是日常开发要做的,但其实意义已经不大了。因为PageHelper的限制,最后封装PageInfo已经属于脱裤放屁的操作。
对于一个分页,最关键的数据列表和总数都已经拿到了,是否封装为PageInfo,其实没有必要,而强行封为PageInfo,反而影响接口的自由定义不利于项目或团队的自定义规范或约定或统一报文