Mybatis物理分页方法详解

Mybatis 的物理分页是应用中的一个难点,特别是配合检索和排序功能叠加时更是如此。

【背景】
项目框架是 SpringMVC+Mybatis, 需求是想采用自定义的分页标签,同时,要尽量少的影响业务程序开发的。
如果你已经使用了JS框架( 如:Ext,EasyUi等)自带的分页机能,是属于前端分页,不在本文讨论范围。

【关于问题】
大多数分页器会使用在查询页面,要考虑以下问题:
1)分页时是要随时带有最近一次查询条件
2)不能影响现有的sql,类似aop的效果
3)mybatis提供了通用的拦截接口,要选择适当的拦截方式和时点
4)尽量少的影响现有service等接口

【关于依赖库】
Google Guava 作为基础工具包
Commons JXPath 用于对象查询 (1/23日版改善后,不再需要)
Jackson 向前台传送Json格式数据转换用

【关于适用数据库】
现在只适用mysql
(如果需要用在其他数据库可参考 paginator的Dialect部分,改动都不大)
首先是Page类,比较简单,保存分页相关的所有信息,涉及到分页算法。虽然“其貌不扬”,但很重要。后面会看到这个page类对象会以“信使”的身份出现在全部与分页相关的地方。

然后是最核心的拦截器了。涉及到了mybatis的核心功能,期间阅读大量mybatis源码几经修改重构,辛苦自不必说。核心思想是将拦截到的select语句,改装成select count(*)语句,执行之得到,总数据数。再根据page中的当前页号算出limit值,拼接到select语句后。为简化代码使用了Commons JXPath 包,做对象查询。

到展示层终于可以轻松些了,使用了文件标签来简化前台开发。采用临时表单提交,CSS使用了Bootstrap。

注意“信使”在这里使出了浑身解数,7个主要的get方法全部用上了。

到这里三个核心模块完成了。然后是拦截器的注册。

【拦截器的注册】需要在mybatis-config.xml 中加入拦截器的配置

【相关代码修改】
首先是后台代码的修改,Controller层由于涉及到查询条件,需要修改的内容较多。
1)入参需增加 pageNo,pageSize 两个参数
2)根据pageNo,pageSize 及你的相对url构造page对象。(
3)最重要的是将你的其他入参(查询条件)保存到page中
4)Service层的方法需要带着page这个对象(最终目的是传递到sql执行的入参,让拦截器识别出该sql需要分页,同时传递页号)
5)将page对象传回Mode中

修改前后

注意:pageSize的缺省值决定该分页的每页数据行数 ,实际项目更通用的方式是使用配置文件指定。Service层拦截器可以自动识别在Map或Bean中的Page对象。如果使用Bean需要在里面增加一个page项目,Map则比较简单,以下是例子。

前台页面方面,由于使用了标签,在适当的位置加一句就够了。

“信使”page在这里进入标签,让分页按钮最终展现。至此,无需修改一句sql,完成分页自动化。

【效果图】

【总结】

现在回过头来看下最开始提出的几个问题:

1)分页时是要随时带有最近一次查询条件

回答:在改造Controller层时,通过将提交参数设置到 Page对象的 Map<String, String> params(单个基本型参数) 和 Map<String, List<String>> paramLists(数组基本型)解决。

顺便提一下,例子中没有涉及参数是Bean的情况,实际应用中应该比较常见。简单的方法是将Bean转换层Map后加入到params。

2)不能影响现有的sql,类似aop的效果

回答:利用Mybatis提供了 Interceptor 接口,拦截后改头换面去的件数并计算limit值,自然能神不知鬼不觉。

3)mybatis提供了通用的拦截接口,要选择适当的拦截方式和时点

回答:@Signature(method = “query”, type = Executor.class, args = {  MappedStatement.class, Object.class, RowBounds.class,  ResultHandler.class }) 只拦截查询语句,其他增删改查不会影响。

4)尽量少的影响现有service等接口

回答:这个自认为本方案做的还不够好,主要是Controller层改造上,感觉代码量还比较大。如果有有识者知道更好的方案还请多指教。

【遗留问题】

1)一个“明显”的性能问题,是每次检索前都要去 select count(*)一次。在很多时候(数据变化不是特别敏感的场景)是不必要的。调整也不难,先Controller参数增加一个 totalRecord 总记录数 ,在稍加修改一下Page相关代码即可。

2)要排序怎么办?本文并未讨论排序,但是方法是类似的。以上面代码为基础,可以较容易地实现一个通用的排序标签。

===================================== 分割线 (1/8)=======================================

对于Controller层需要将入参传入Page对象的问题已经进行了改善,思路是自动从HttpServletRequest 类中提取入残,减低了分页代码的侵入性

再次改善,使用ThreadLocal类封装Page对象,让Service层等无需传Page对象,减小了侵入性。拦截器也省去了查找Page对象的动作,性能也同时改善。整体代码改动不大。

今天比较闲,顺便聊下这个分页的最终版,当然从来只有不断变化的需求,没有完美的方案,这里所说的最终版其实是一个优化后的“零侵入”的方案。为避免代码混乱还是只介绍思路。在上一个版本(1/23版)基础上有两点改动:

一是增加一个配置文件,按Url 配置初始的每页行数。如下面这样(pagesize 指的是每页行数):

二是增加一个过滤器,并将剩下的位于Control类中 唯一侵入性的分页相关代码移入过滤器。发现当前的 Url  在配置文件中有匹配是就构造Page对象,并加入到Response中。

使用最终版后,对于开发者需要分页时,只要在配置文件中加一行,并在前端页面上加一个分页标签即可,其他代码,SQL等都不需要任何改动,可以说简化到了极限。

【技术列表】

总结下最终方案用到的技术:

  • Mybatis 提供的拦截器接口实现(实现分页sql自动 select count 及limit 拼接)
  • Servlet过滤器+ThreadLocal  (生成线程共享的Page对象)
  • 标签文件   (实现前端共通的分页效果)
  • 临时表单提交 (减少页面体积)

【其他分页方案比较】

时下比较成熟的 JPA 的分页方案,(主要应用在 Hibernate + Spring Data 的场合),主要切入点在DAO层,而Controller等各层接口依然需要带着pageNumber,pageSize 这些的参数,另外框架开发者还要掌握一些必须的辅助类,如:

org.springframework.data.repository.PagingAndSortingRepository    可分页DAO基类

org.springframework.data.domain.Page            抽取结果封装类

org.springframework.data.domain.Pageable     分页信息类

比较来看 本方案 做到了分页与业务逻辑的完全解耦,开发者无需关注分页,全部通过配置实现。通过这个例子也可以反映出Mybatis在底层开发上有其独特的优势。

【备选方案】

最后再闲扯下,上面的最终案是基于 Url 配置的,其实也可以基于方法加自定义注解来做。这样配置文件省了,但是要增加一个注解解析类。注解中参数 为初始的每页行数。估计注解fans会喜欢,如下面的样子:

同样与过滤器配合使用,只是注解本身多少还是有“侵入性”。在初始行数基本不会变更时,这个比较直观的方案也是不错的选择。大家自行决定吧。

发表回复