一.借助数组进行分页

原理:进行数据库查询操作时,获取到数据库中所有满足条件的记录,保存在应用的临时数组中,再通过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接口,并且定义分页方法:

  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方法,获取到两个索引间的所有数据。

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

二.借助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分页实现。

sql分页语句如下:

1 select * from table limit index, pageSize;

所以在service中计算出currIndex:要开始查询的第一条记录的索引。

结果:

从输出结果可以看出和数组分页的结果是一致的,因此sql语句的分页也是没问题的。

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

三.拦截器分页

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

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

上面的所有数据都可以通过反射拿到。

几个重要的参数:
Configuration:所有配置的相关信息。
ResultSetHandler:用于拦截执行结果的组装。
ParameterHandler:拦截执行Sql的参数的组装。
Executor:执行Sql的全过程,包括组装参数、组装结果和执行Sql的过程。
BoundSql:执行的Sql的相关信息。

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

//获取进行数据库操作时管理参数的handler

1 ParameterHandler parameterHandler = (ParameterHandler) MetaObjectHandler.getValue("delegate.parameterHandler");
2 //获取请求时的参数
3 Map<String, Object> paraObject = (Map<String, Object>) parameterHandler.getParameterObject();
4 //也可以这样获取
5 //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 @Override
2 public void setProperties(Properties properties) {
3     String limit1 = properties.getProperty("limit", "10");
4     this.pageSize = Integer.valueOf(limit1);
5     this.dbType = properties.getProperty("dbType", "mysql");
6 }

到这里,有关拦截器的相关知识就讲解的差不多了,接下来就需要测试,是否我们这样写真的有效??

首先还是添加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 }

结果:

可见和上面两种分页的效果是一样的。

四.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
 8 public RowBounds() {
 9     this.offset = 0;
10     this.limit = 2147483647;
11 }
12
13 public RowBounds(int offset, int limit) {
14     this.offset = offset;
15     this.limit = limit;
16 }
17
18 public int getOffset() {
19     return this.offset;
20 }
21
22 public int getLimit() {
23     return this.limit;
24 }

结论:从上面四种sql分页的实现方式可以看出,通过RowBounds实现是最简便的,但是通过拦截器的实现方式是最优的方案。只需一次编写,所有的分页方法共同使用,还可以避免多次配置时的出错机率,需要修改时也只需要修改这一个文件,一劳永逸。而且是我们自己实现的,便于我们去控制和增加一些逻辑处理,使我们在外层更简单的使用。同时也不会出现数组分页和RowBounds分页导致的性能问题。当然,具体情况可以采取不同的解决方案。数据量小时,RowBounds不失为一种好办法。但是数据量大时,实现拦截器就很有必要了。

JavaWeb实现分页的四种方法相关推荐

  1. (转).NET导出Excel的四种方法及评测

    .NET导出Excel的四种方法及评测 导出Excel是.NET的常见需求,开源社区.市场上,都提供了不少各式各样的Excel操作相关包.本文,我将使用NPOI.EPPlus.OpenXML.Aspo ...

  2. PHP取整数函数常用的四种方法

    PHP取整数函数常用的四种方法: 1.直接取整,舍弃小数,保留整数:intval():  2.四舍五入取整:round():  3.向上取整,有小数就加1:ceil():  4.向下取整:floor( ...

  3. java 中lock,java中lock获取锁的四种方法

    在java接口中会存放着许多方法,方便线程使用时的直接调用.对于lock接口大家都不陌生,我们已经初步对概念进行了理解.那么在获取锁的方法上想必还不是很清楚.下面我们就lock获取锁的四种方法分别进行 ...

  4. 简单介绍C语言使用四种方法初始化结构体

    这篇文章说明了什么是结构体,介绍了结构体的概念和使用优点,在C语言中如何使用和初始化结构体方法,通过详细的代码展开进行说明,希望该篇文章对你有所帮助 什么是结构体 在实际问题中,一组数据往往有很多种不 ...

  5. svg鼠标响应事件的四种方法(其中两种可支持火狐)

    svg鼠标响应事件的四种方法 鼠标响应事件的四种方法,以click事件为例. Mouse Events - SMIL <?xml version="1.0" encoding ...

  6. oracle调整字段精度的四种方法

    oracle调整字段精度的四种方法: 01_执行用户_ddl/dml_表名_注释(建表/授权/同义词) 调整方式一:(精度只能调大不能调小) alter table table_name_a MODI ...

  7. 详解Java解析XML的四种方法

    http://developer.51cto.com  2009-03-31 13:12  cnlw1985  javaeye  我要评论(8) XML现在已经成为一种通用的数据交换格式,平台的无关性 ...

  8. linux安装IPython四种方法

    IPython是Python的交互式Shell,提供了代码自动补完,自动缩进,高亮显示,执行Shell命令等非常有用的特性.特别是它的代码补完功能,例如:在输入zlib.之后按下Tab键,IPytho ...

  9. 查看mysql版本的四种方法

    2019独角兽企业重金招聘Python工程师标准>>> 查看mysql版本的四种方法 1:在终端下:mysql -V. 以下是代码片段: [shengting@login ~]$ m ...

最新文章

  1. 如何在搜索结果出来之前,让页面显示“等待中。。。”
  2. vs2013 openmp例子
  3. 关于默认jdk设置问题
  4. java中读取某路径下的文本内容
  5. 计算机公开课推荐 2019.8
  6. 【求助】测试XCode v8.0的正向反向功能
  7. 数据预处理第2讲:非线性变换
  8. 刚做微商引流太慢怎么办?微商没有客源怎么办,微商引流需要注意什么
  9. mysql中身份证号判断男女人数
  10. 夜神安卓模拟器怎么设置代理
  11. 服务器怎么使用无线网卡,无线上网卡怎么用
  12. 分享一些数据库使用的心得
  13. origin作统计图(两个x正轴,一个y轴效果)
  14. 材料研发转行,转什么比较好
  15. HDU 5514 Frogs 容斥
  16. MyEclipse2014安装包附注册破解包、eclipse安装包
  17. 想要提高企业分账管理,简单分账系统如何做到?
  18. 基于联发科(MTK)MT8735平台的安卓4G全网通核心板
  19. 中国哲学简史学习笔记
  20. php解三元一次方程,三元一次方程的求解

热门文章

  1. 一年暴涨近20倍,比特币是郁金香泡沫还是庞氏骗局?
  2. 同一份数据,Redis为什么要存两次?
  3. 面试官:聊一聊 Spring Boot 服务监控机制
  4. Spring Boot + Redis 操作多种实现
  5. 推荐一款 Spring Boot 的 HTTP 客户端框架
  6. 再见Spring!下一个开源框架更香!
  7. 服务被干爆了!竟然是日志的锅!!
  8. 最强 Java Redis 客户端
  9. 如何优雅地展示机器学习项目!
  10. 谈谈计算机行业的秋招和春招