前言

学习hibernate & mybatis等持久层框架的时候,不外乎对数据库的增删改查操作。而使用最多的当是数据库的查找操作, 而当数据库数据过多时,符合查找条件的数据可能也会是很庞大的数据。往往在这个时候,我们都不会希望一次性的将所有的数据一起性读取出来,并且显示在UI界面上。常用的操作,就是对查找到的数据进行分页,每次处理小部分数据。这样每次处理的数据量就会在可控的范围,UI的展示也会很协调。

问题

面对上面的问题,今天我们就来进行基于mybatis和MySql进行分页功能的实现。常见的数据分页有哪几种实现??基于数组的分页实现?基于sql语句的分页实现?还是通过拦截器进行数据分页功能?还是通过RowBounds参数进行物理分页?几种都是常用的分页实现原理,接下来就按照数组、sql语句,拦截器和RowBounds的方式介绍分页功能。

一、借助数组进行分页

进行数据库查询操作时,获取到数据库中所有满足条件的记录,保存在应用的临时数组中,再通过List的subList方法,获取到满足条件的所有记录。

实现:

首先在dao层,创建StudentMapper接口,用于对数据库的操作。在接口中定义通过数组分页的查询方法,如下所示:

  1. List<Student> queryStudentsByArray();

方法很简单,就是获取所有的数据,通过list接收后进行分页操作。
创建StudentMapper.xml文件,编写查询的sql语句:

  1. <select id="queryStudentsByArray" resultMap="studentmapper">
  2. select * from student
  3. </select>

可以看出再编写sql语句的时候,我们并没有作任何分页的相关操作。这里是查询到所有的学生信息。

接下来在service层获取数据并且进行分页实现:
定义IStuService接口,并且定义分页方法:

  1. List<Student> queryStudentsByArray(int currPage, int pageSize);

通过接收currPage参数表示显示第几页的数据,pageSize表示每页显示的数据条数。

创建IStuService接口实现类StuServiceIml对方法进行实现,对获取到的数组通过currPage和pageSize进行分页:

  1. @Override
  2. public List<Student> queryStudentsByArray(int currPage, int pageSize) {
  3. List<Student> students = studentMapper.queryStudentsByArray();
  4. //从第几条数据开始
  5. int firstIndex = (currPage - 1) * pageSize;
  6. //到第几条数据结束
  7. int lastIndex = currPage * pageSize;
  8. return students.subList(firstIndex, lastIndex);
  9. }

通过subList方法,获取到两个索引间的所有数据(API方法)。
最后在controller中创建测试方法:

  1. @ResponseBody
  2. @RequestMapping("/student/array/{currPage}/{pageSize}")
  3. public List<Student> getStudentByArray(
  4. @PathVariable("currPage") int currPage, @PathVariable("pageSize") int pageSize) {
  5. List<Student> student = StuServiceIml.queryStudentsByArray(currPage, pageSize);
  6. return student;
  7. }

通过用户传入的currPage和pageSize获取指定数据。

缺点:

数据库查询并返回所有的数据,而我们需要的只是极少数符合要求的数据。当数据量少时,还可以接受。当数据库数据量过大时,每次查询对数据库和程序的性能都会产生极大的影响。

二、借助Sql语句进行分页

在了解到通过数组分页的缺陷后,我们发现不能每次都对数据库中的所有数据都检索。然后在程序中对获取到的大量数据进行二次操作,这样对空间和性能都是极大的损耗。所以我们希望能直接在数据库语言中只检索符合条件的记录,不需要在通过程序对其作处理。这时,Sql语句分页技术横空出世。

实现:

通过sql语句实现分页也是非常简单的,只是需要改变我们查询的语句就能实现了,即在sql语句后面添加limit分页语句。
首先还是在StudentMapper接口中添加sql语句查询的方法,如下:

  1. List<Student> queryStudentsBySql(Map<String,Object> data);

然后在StudentMapper.xml文件中编写sql语句通过limiy关键字进行分页:

  1. <select id="queryStudentsBySql" parameterType="map" resultMap="studentmapper">
  2. select * from student limit #{currIndex} , #{pageSize}
  3. </select>

接下来还是在IStuService接口中定义方法,并且在StuServiceIml中对sql分页实现。

  1. List<Student> queryStudentsBySql(int currPage, int pageSize);
  1. @Override
  2. public List<Student> queryStudentsBySql(int currPage, int pageSize) {
  3. Map<String, Object> data = new HashedMap();
  4. data.put("currIndex", (currPage-1)*pageSize);
  5. data.put("pageSize", pageSize);
  6. return studentMapper.queryStudentsBySql(data);
  7. }

sql分页语句如下:select * from table limit index, pageSize;
所以在service中计算出currIndex:要开始查询的第一条记录的索引。

缺点:

虽然这里实现了按需查找,每次检索得到的是指定的数据。但是每次在分页的时候都需要去编写limit语句,很冗余。而且不方便统一管理,维护性较差。所以我们希望能够有一种更方便的分页实现。

三、拦截器分页

上面提到的数组分页和sql语句分页都不是我们今天讲解的重点,今天需要实现的是利用拦截器达到分页的效果。自定义拦截器实现了拦截所有以ByPage结尾的查询语句,并且利用获取到的分页相关参数统一在sql语句后面加上limit分页的相关语句,一劳永逸。不再需要在每个语句中单独去配置分页相关的参数了。。

类似博客

mybatis精通之路之插件分页(拦截器)进阶

首先我们看一下拦截器的具体实现,在这里我们需要拦截所有以ByPage结尾的所有查询语句,因此要使用该拦截器实现分页功能,那么再定义名称的时候需要满足它拦截的规则(以ByPage结尾),如下所示:

  1. package com.cbg.interceptor;
  2. import org.apache.ibatis.executor.Executor;
  3. import org.apache.ibatis.executor.parameter.ParameterHandler;
  4. import org.apache.ibatis.executor.resultset.ResultSetHandler;
  5. import org.apache.ibatis.executor.statement.StatementHandler;
  6. import org.apache.ibatis.mapping.MappedStatement;
  7. import org.apache.ibatis.plugin.*;
  8. import org.apache.ibatis.reflection.MetaObject;
  9. import org.apache.ibatis.reflection.SystemMetaObject;
  10. import java.sql.Connection;
  11. import java.util.Map;
  12. import java.util.Properties;
  13. /**
  14. * @Intercepts 说明是一个拦截器
  15. * @Signature 拦截器的签名
  16. * type 拦截的类型 四大对象之一( Executor,ResultSetHandler,ParameterHandler,StatementHandler)
  17. * method 拦截的方法
  18. * args 参数
  19. */
  20. @Intercepts({@Signature(type = StatementHandler.class, method = "prepare",
  21. args = {Connection.class, Integer.class})})
  22. public class MyPageInterceptor implements Interceptor {
  23. //每页显示的条目数
  24. private int pageSize;
  25. //当前现实的页数
  26. private int currPage;
  27. private String dbType;
  28. @Override
  29. public Object intercept(Invocation invocation) throws Throwable {
  30. //获取StatementHandler,默认是RoutingStatementHandler
  31. StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
  32. //获取statementHandler包装类
  33. MetaObject MetaObjectHandler = SystemMetaObject.forObject(statementHandler);
  34. //分离代理对象链
  35. while (MetaObjectHandler.hasGetter("h")) {
  36. Object obj = MetaObjectHandler.getValue("h");
  37. MetaObjectHandler = SystemMetaObject.forObject(obj);
  38. }
  39. while (MetaObjectHandler.hasGetter("target")) {
  40. Object obj = MetaObjectHandler.getValue("target");
  41. MetaObjectHandler = SystemMetaObject.forObject(obj);
  42. }
  43. //获取连接对象
  44. //Connection connection = (Connection) invocation.getArgs()[0];
  45. //获取StatementHandler的实现类
  46. //object.getValue("delegate");
  47. //获取查询接口映射的相关信息
  48. MappedStatement mappedStatement =
  49. (MappedStatement) MetaObjectHandler.getValue("delegate.mappedStatement");
  50. String mapId = mappedStatement.getId();
  51. //statementHandler.getBoundSql().getParameterObject();
  52. //拦截以.ByPage结尾的请求,分页功能的统一实现
  53. if (mapId.matches(".+ByPage$")) {
  54. //获取进行数据库操作时管理参数的handler
  55. ParameterHandler parameterHandler =
  56. (ParameterHandler) MetaObjectHandler.getValue("delegate.parameterHandler");
  57. //获取请求时的参数
  58. Map<String, Object> paraObject =
  59. (Map<String, Object>) parameterHandler.getParameterObject();
  60. //也可以这样获取
  61. //paraObject = (Map<String, Object>) statementHandler.getBoundSql().getParameterObject();
  62. //参数名称和在service中设置到map中的名称一致
  63. currPage = (int) paraObject.get("currPage");
  64. pageSize = (int) paraObject.get("pageSize");
  65. String sql = (String) MetaObjectHandler.getValue("delegate.boundSql.sql");
  66. //也可以通过statementHandler直接获取
  67. //sql = statementHandler.getBoundSql().getSql();
  68. //构建分页功能的sql语句
  69. String limitSql;
  70. sql = sql.trim();
  71. limitSql = sql + " limit " + (currPage - 1) * pageSize + "," + pageSize;
  72. //将构建完成的分页sql语句赋值个体'delegate.boundSql.sql',偷天换日
  73. MetaObjectHandler.setValue("delegate.boundSql.sql", limitSql);
  74. }
  75. //调用原对象的方法,进入责任链的下一级
  76. return invocation.proceed();
  77. }
  78. //获取代理对象
  79. @Override
  80. public Object plugin(Object o) {
  81. //生成object对象的动态代理对象
  82. return Plugin.wrap(o, this);
  83. }
  84. //设置代理对象的参数
  85. @Override
  86. public void setProperties(Properties properties) {
  87. //如果项目中分页的pageSize是统一的,也可以在这里统一配置和获取,
  88. //这样就不用每次请求都传递pageSize参数了。参数是在配置拦截器时配置的。
  89. String limit1 = properties.getProperty("limit", "10");
  90. this.pageSize = Integer.valueOf(limit1);
  91. this.dbType = properties.getProperty("dbType", "mysql");
  92. }
  93. }

上面即是拦截器功能的实现,在intercept方法中获取到select标签和sql语句的相关信息,拦截所有以ByPage结尾的select查询,并且统一在查询语句后面添加limit分页的相关语句,统一实现分页功能。

重点详解:
StatementHandler是一个接口,而我们在代码中通过StatementHandler statementHandler = (StatementHandler) invocation.getTarget();获取到的是StatementHandler默认的实现类RoutingStatementHandler。而RoutingStatementHandler只是一个中间代理,他不会提供具体的方法。那你可能会纳闷了,拦截器中基本上是依赖statementHandler获取各种对象和属性的,没有具体属性和方法怎么行??接着看下面代码:

  1. private final StatementHandler delegate;
  2. public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
  3. switch(RoutingStatementHandler.SyntheticClass_1.$SwitchMap$org$apache$ibatis$mapping$StatementType[ms.getStatementType().ordinal()]) {
  4. case 1:
  5. this.delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
  6. break;
  7. case 2:
  8. this.delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
  9. break;
  10. case 3:
  11. this.delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
  12. break;
  13. default:
  14. throw new ExecutorException("Unknown statement type: " + ms.getStatementType());
  15. }
  16. }

原来它是通过不同的MappedStatement创建不同的StatementHandler实现类对象处理不同的情况。这里的到的StatementHandler实现类才是真正服务的。看到这里,你可能就会明白MappedStatement mappedStatement = (MappedStatement) MetaObjectHandler.getValue(“delegate.mappedStatement”);中delegate的来源了吧。至于为什么要这么去获取,后面我们会说道。
拿到statementHandler后,我们会通过MetaObject MetaObjectHandler = SystemMetaObject.forObject(statementHandler);去获取它的包装对象,通过包装对象去获取各种服务。
MetaObject:mybatis的一个工具类,方便我们有效的读取或修改一些重要对象的属性。四大对象(ResultSetHandler,ParameterHandler,Executor和statementHandler)提供的公共方法很少,要想直接获取里面属性的值很困难,但是可以通过MetaObject利用一些技术(内部反射实现)很轻松的读取或修改里面的数据。
接下来说说:MappedStatement mappedStatement = (MappedStatement) MetaObjectHandler.getValue(“delegate.mappedStatement”);
上面提到为什么要这么去获取MappedStatement对象??在RoutingStatementHandler中delegate是私有的(private final StatementHandler delegate;),有没有共有的方法去获取。所以这里只有通过反射来获取啦。
MappedStatement是保存了xxMapper.xml中一个sql语句节点的所有信息的包装类,可以通过它获取到节点中的所有信息。在示例中我们拿到了id值,也就是方法的名称,通过名称区拦截所有需要分页的请求。
通过StatementHandler的包装类,不光能拿到MappedStatement,还可以拿到下面的数据:

  1. public abstract class BaseStatementHandler implements StatementHandler {
  2. protected final Configuration configuration;
  3. protected final ObjectFactory objectFactory;
  4. protected final TypeHandlerRegistry typeHandlerRegistry;
  5. protected final ResultSetHandler resultSetHandler;
  6. protected final ParameterHandler parameterHandler;
  7. protected final Executor executor;
  8. protected final MappedStatement mappedStatement;
  9. protected final RowBounds rowBounds;
  10. protected BoundSql boundSql;
  11. protected BaseStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
  12. this.configuration = mappedStatement.getConfiguration();
  13. this.executor = executor;
  14. this.mappedStatement = mappedStatement;
  15. this.rowBounds = rowBounds;
  16. this.typeHandlerRegistry = this.configuration.getTypeHandlerRegistry();
  17. this.objectFactory = this.configuration.getObjectFactory();
  18. if(boundSql == null) {
  19. this.generateKeys(parameterObject);
  20. boundSql = mappedStatement.getBoundSql(parameterObject);
  21. }
  22. this.boundSql = boundSql;
  23. this.parameterHandler = this.configuration.newParameterHandler(mappedStatement, parameterObject, boundSql);
  24. this.resultSetHandler = this.configuration.newResultSetHandler(executor, mappedStatement, rowBounds, this.parameterHandler, resultHandler, boundSql);
  25. }
  26. }

上面的所有数据都可以通过反射拿到。几个重要的参数:
Configuration:所有配置的相关信息。
ResultSetHandler:用于拦截执行结果的组装。
ParameterHandler:拦截执行Sql的参数的组装。
Executor:执行Sql的全过程,包括组装参数、组装结果和执行Sql的过程。
BoundSql:执行的Sql的相关信息。

接下来我们通过如下代码拿到请求时的map对象(反射)。

  1. //获取进行数据库操作时管理参数的handler
  2. ParameterHandler parameterHandler = (ParameterHandler) MetaObjectHandler
  3. .getValue("delegate.parameterHandler");
  4. //获取请求时的参数
  5. Map<String, Object> paraObject = (Map<String, Object>) parameterHandler.getParameterObject();
  6. //也可以这样获取
  7. //paraObject = (Map<String, Object>) statementHandler.getBoundSql().getParameterObject();

拿到我们需要的currPage和pageSize参数后,就是组装分页查询的sql语句’limitSql‘了。
最后通过MetaObjectHandler.setValue(“delegate.boundSql.sql”, limitSql);将原始的sql语句替换成我们新的分页语句,完成偷天换日的功能,接下来让代码继续执行。
编写好拦截器后,需要注册到项目中,才能发挥它的作用。在mybatis的配置文件中,添加如下代码:

  1. <plugins>
  2. <plugin interceptor="com.cbg.interceptor.MyPageInterceptor">
  3. <property name="limit" value="10"/>
  4. <property name="dbType" value="mysql"/>
  5. </plugin>
  6. </plugins>

如上所示,还能在里面配置一些属性,在拦截器的setProperties方法中可以获取配置好的属性值。如项目分页的pageSize参数的值固定,我们就可以配置在这里了,以后就不需要每次传入pageSize了,读取方式如下:

  1. //读取配置的代理对象的参数
  2. @Override
  3. public void setProperties(Properties properties) {
  4. String limit1 = properties.getProperty("limit", "10");
  5. this.pageSize = Integer.valueOf(limit1);
  6. this.dbType = properties.getProperty("dbType", "mysql");
  7. }

到这里,有关拦截器的相关知识就讲解的差不多了,接下来就需要测试,是否我们这样写真的有效??
首先还是添加dao层的方法和xml文件的sql语句配置,注意项目中拦截的是以ByPage结尾的请求,所以在这里,我们的方法名称也以此结尾:
方法

  1. List<Student> queryStudentsByPage(Map<String,Object> data);

xml文件的select语句

  1. <select id="queryStudentsByPage" parameterType="map" resultMap="studentmapper">
  2. select * from student
  3. </select>

可以看出,这里我们就不需要再去手动配置分页语句了。
接下来是service层的接口编写和实现方法:
方法:

  1. List<Student> queryStudentsByPage(int currPage,int pageSize);

实现:

  1. @Override
  2. public List<Student> queryStudentsByPage(int currPage, int pageSize) {
  3. Map<String, Object> data = new HashedMap();
  4. data.put("currPage", currPage);
  5. data.put("pageSize", pageSize);
  6. return studentMapper.queryStudentsByPage(data);
  7. }

这里我们虽然传入了currPage和pageSize两个参数,但是在sql的xml文件中并没有使用,直接在拦截器中获取到统一使用。
最后编写controller的测试代码:

  1. @ResponseBody
  2. @RequestMapping("/student/page/{currPage}/{pageSize}")
  3. public List<Student> getStudentByPage(@PathVariable("currPage") int currPage, @PathVariable("pageSize") int pageSize) {
  4. List<Student> student = StuServiceIml.queryStudentsByPage(currPage, pageSize);
  5. return student;
  6. }

测试:
在浏览器输入:http://localhost:8080/student/student/page/1/2
结果:
可见和上面两种分页的效果是一样的。

四、RowBounds实现分页

通过RowBounds实现分页和通过数组方式分页原理差不多,都是一次获取所有符合条件的数据,然后在内存中对大数据进行操作,实现分页效果。只是数组分页需要我们自己去实现分页逻辑,这里更加简化而已。
存在问题:一次性从数据库获取的数据可能会很多,对内存的消耗很大,可能导致性能变差,甚至引发内存溢出。
适用场景:在数据量很大的情况下,建议还是适用拦截器实现分页效果。RowBounds建议在数据量相对较小的情况下使用。
简单介绍:这是代码实现上最简单的一种分页方式,只需要在dao层接口中要实现分页的方法中加入RowBounds参数,然后在service层通过offset(从第几行开始读取数据,默认值为0)和limit(要显示的记录条数,默认为java允许的最大整数:2147483647)两个参数构建出RowBounds对象,在调用dao层方法的时,将构造好的RowBounds传进去就能轻松实现分页效果了。

实现:

dao层接口方法:

  1. //加入RowBounds参数
  2. public List<UserBean> queryUsersByPage(String userName, RowBounds rowBounds);

然后在service层构建RowBounds,调用dao层方法:

  1. @Override
  2. @Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.SUPPORTS)
  3. public List<RoleBean> queryRolesByPage(String roleName, int start, int limit) {
  4. return roleDao.queryRolesByPage(roleName, new RowBounds(start, limit));
  5. }

RowBounds就是一个封装了offset和limit简单类,如下所示:

  1. public class RowBounds {
  2. public static final int NO_ROW_OFFSET = 0;
  3. public static final int NO_ROW_LIMIT = 2147483647;
  4. public static final RowBounds DEFAULT = new RowBounds();
  5. private int offset;
  6. private int limit;
  7. public RowBounds() {
  8. this.offset = 0;
  9. this.limit = 2147483647;
  10. }
  11. public RowBounds(int offset, int limit) {
  12. this.offset = offset;
  13. this.limit = limit;
  14. }
  15. public int getOffset() {
  16. return this.offset;
  17. }
  18. public int getLimit() {
  19. return this.limit;
  20. }
  21. }

只需要这两步操作,就能轻松实现分页效果了,是不是很神奇。但却不简单,内部是怎么实现的??给大家提供一个简单的思路:RowBounds分页简单原理

五、结论

从上面四种sql分页的实现方式可以看出,通过RowBounds实现是最简便的,但是通过拦截器的实现方式是最优的方案。只需一次编写,所有的分页方法共同使用,还可以避免多次配置时的出错机率,需要修改时也只需要修改这一个文件,一劳永逸。而且是我们自己实现的,便于我们去控制和增加一些逻辑处理,使我们在外层更简单的使用。同时也不会出现数组分页和RowBounds分页导致的性能问题。当然,具体情况可以采取不同的解决方案。数据量小时,RowBounds不失为一种好办法。但是数据量大时,实现拦截器就很有必要了。
到这里,mybatis的分页原理和全部实现过程都完成了,还有不清楚的可以自己去看一下mybatis的源码,按照这个思路去阅读还是比较清晰的。这里只是对插件(拦截器)实现分页做了个简单的介绍,只是简单的分页功能,还很简陋。