分页简介
分页功能在网页中是非常常见的一个功能, 其作用也就是将数据分割成多个页面来进行显示.
使用场景: 当取到的数据量达到一定的时候, 就需要使用分页来进行数据分割.
当我们不使用分页功能的时候, 会面临许多的问题:
客户端的问题: 如果数据量太多, 都显示在同一个页面的话, 会因为页面太长严重影响到用户的体验, 也不便于操作, 也会出现加载太慢的问题.
服务端的问题: 如果数据量太多, 可能会造成内存溢出, 而且一次请求携带的数据太多, 对服务器的性能也是一个考验.
分页的分类
分页的实现分为真分页和假分页两种, 也就是物理分页和逻辑分页.
1. 真分页(物理分页):
实现原理:
SELECT * FROM xxx [WHERE...] LIMIT #{param1}, #{param2}
第一个参数是开始数据的索引位置
第二个参数是要查询多少条数据
优点: 不会造成内存溢出
缺点: 翻页的速度比较慢
2. 假分页(逻辑分页):
实现原理: 一次性将所有的数据查询出来放在内存之中, 每次需要查询的时候就直接从内存之中去取出相应索引区间的数据
优点: 分页的速度比较快
缺点: 可能造成内存溢出
传统的分页方式
对于假分页的实现方式很简单, 只需要准备一个集合保存从数据库中取出的所有数据, 然后根据当前页面的码数, 取出对应范围的数据显示就好了, 我们这里基于物理分页来实现.
分页的原理
页面中的数据有:
结果集: 通过 SQL 语句查询得来的 --List
分页条中的数据有:
当前页: 用户传递到后台 --currentPage
总页数: 计算的来 --totalPage
上一页: 计算的来 --prePage
下一页: 计算的来 --nextPage
尾页: 计算的来(总页数)--lastPage
页面大小(即每一页显示的条数): 用户传递到后台 --count
总条数: 通过 SQL 语句查询得来的 --totalCount
可以发现页面功能中需要用到的数据有两个是需要通过 SQL 语句查询得来的: 一个是页面中显示的数据 List , 另一个是数据的总条数 totalCount, 分别对应以下两条 SQL 语句:
- SELECT * FROM student LIMIT #{param1}, #{param2}
- SELECT COUNT(*) FROM student
通过计算得到的数据有:
总页数: totalPage
总页数 = 总条数 % 页面大小 == 0 ? 总条数 / 页面大小 : 总条数 / 页面大小 + 1
上一页: prePage
上一页 = 当前页 - 1> = 1 ? 当前页 - 1 : 1
下一页: nextPage
下一页 = 当前页 + 1 <= totalPage ? 当前页 + 1 : totalPage
尾页: lastPage
尾页 = 总条数 % 页面大小 == 0 ? 总条数 - 页面大小 : 总条数 - 总条数 % 页面大小
用户传递的数据:
当前页: currentPage
页面大小: count
所有我们可以创建一个 Page 工具类备用:
- public class Page {
- int start; // 开始数据的索引
- int count; // 每一页的数量
- int total; // 总共的数据量
- /**
- * 提供一个构造方法
- * @param start
- * @param count
- */
- public Page(int start, int count) {
- super();
- this.start = start;
- this.count = count;
- }
- /**
- * 判断是否有上一页
- * @return
- */
- public boolean isHasPreviouse(){
- if(start==0)
- return false;
- return true;
- }
- /**
- * 判断是否有下一页
- * @return
- */
- public boolean isHasNext(){
- if(start==getLast())
- return false;
- return true;
- }
- /**
- * 计算得到总页数
- * @return
- */
- public int getTotalPage(){
- int totalPage;
- // 假设总数是 50, 是能够被 5 整除的, 那么就有 10 页
- if (0 == total % count)
- totalPage = total /count;
- // 假设总数是 51, 不能够被 5 整除的, 那么就有 11 页
- else
- totalPage = total / count + 1;
- if(0==totalPage)
- totalPage = 1;
- return totalPage;
- }
- /**
- * 计算得到尾页
- * @return
- */
- public int getLast(){
- int last;
- // 假设总数是 50, 是能够被 5 整除的, 那么最后一页的开始就是 45
- if (0 == total % count)
- last = total - count;
- // 假设总数是 51, 不能够被 5 整除的, 那么最后一页的开始就是 50
- else
- last = total - total % count;
- last = last<0?0:last;
- return last;
- }
- /* getter and setter */
- }
前台实现分页设计
首先我们在前台需要完成我们分页条的设计, 这里可以直接引入 Bootstrap 来完成:
上面是使用 Bootstrap 实现一个分页条的简单例子, 如果不熟悉的童鞋可以去菜鸟教程中查看: 点这里 http://www.runoob.com/bootstrap/bootstrap-pagination.html
简单版本的分页条
为了便于理解, 我们先来实现一个简单版本的分页条吧:
首页超链: 指向了 start 为 0 的首页
- <li>
- <a href="?page.start=0">
- <span>«</span>
- </a>
- </li>
上一页超链:
- <li>
- <a href="?page.start=${page.start-page.count}">
- <span></span>
- </a>
- </li>
下一页超链:
- <li>
- <a href="?page.start=${page.start+page.count}">
- <span></span>
- </a>
- </li>
最后一页超链: 指向了最后一页
- <li>
- <a href="?page.start=${page.last}">
- <span>»</span>
- </a>
- </li>
中间页:
- <c:forEach begin="0" end="${page.totalPage-1}" varStatus="status">
- <li>
- <a href="?page.start=${status.index*page.count}" class="current">${status.count}</a>
- </li>
- </c:forEach>
所以写完看起来会是这样子的:
- <nav>
- <ul class="pagination">
- <li>
- <a href="?page.start=0">
- <span>«</span>
- </a>
- </li>
- <li>
- <a href="?page.start=${page.start-page.count}">
- <span></span>
- </a>
- </li>
- <c:forEach begin="0" end="${page.totalPage-1}" varStatus="status">
- <li>
- <a href="?page.start=${status.index*page.count}" class="current">${status.count}</a>
- </li>
- </c:forEach>
- <li>
- <a href="?page.start=${page.start+page.count}">
- <span></span>
- </a>
- </li>
- <li>
- <a href="?page.start=${page.last}">
- <span>»</span>
- </a>
- </li>
- </ul>
- </nav>
存在的问题:
没有边界判断, 即在首页仍然可以点击前一页, 不符合逻辑也影响用户体验
会显示完所有的分页, 即如果 totalPage 有 50 页, 那么分页栏将会显得特别长, 影响体验
改良版本的分页条
1. 写好头和尾
- <nav class="pageDIV">
- <ul class="pagination">
- .....
- </ul>
- </nav>
2. 写好 « 这两个功能按钮
使用 <c:if > 标签来增加边界判断, 如果没有前面的页码了则设置为 disable 状态
- <li <c:if test="${!page.hasPreviouse}">class="disabled"</c:if>>
- <a href="?page.start=0">
- <span>«</span>
- </a>
- </li>
- <li <c:if test="${!page.hasPreviouse}">class="disabled"</c:if>>
- <a href="?page.start=${page.start-page.count}">
- <span></span>
- </a>
- </li>
再通过 JavaScrip 代码来完成禁用功能:
- <script>
- $(function () {
- $("ul.pagination li.disabled a").click(function () {
- return false;
- });
- });
- </script>
3. 完成中间页码的编写
- <c:forEach begin="0" end="${page.totalPage-1}" varStatus="status">
- <c:if test="${status.count*page.count-page.start<=30 && status.count*page.count-page.start>=-10}">
- <li <c:if test="${status.index*page.count==page.start}">class="disabled"</c:if>>
- <a
- href="?page.start=${status.index*page.count}"
- <c:if test="${status.index*page.count==page.start}">class="current"</c:if>
- >${status.count}</a>
- </li>
- </c:if>
- </c:forEach>
从 0 循环到 page.totalPage - 1 ,varStatus 相当于是循环变量
status.count 是从 1 开始遍历
status.index 是从 0 开始遍历
要求: 显示当前页码的前两个和后两个就可, 例如当前页码为 3 的时候, 就显示 1 2 3(当前页) 4 5 的页码
理解测试条件:
-10 <= 当前页 * 每一页显示的数目 - 当前页开始的数据编号 <= 30
只要理解了这个判断条件, 其他的就都好理解了
注意: 测试条件是需要根据项目的需求动态改变的, 不是万能的!
后台中的分页
首页在项目中引入上面提到的 Page 工具类, 然后我们在 DAO 类中使用 LIMIT 关键字来查询数据库中的信息:
- public List<Student> list() {
- return list(0, Short.MAX_VALUE);
- }
- public List<Student> list(int start, int count) {
- List<Student> students = new ArrayList<>();
- String sql = "SELECT * FROM student ORDER BY student_id desc limit ?,?";
- try (Connection c = DBUtil.getConnection(); PreparedStatement ps = c.prepareStatement(sql)) {
- ps.setInt(1, start);
- ps.setInt(2, count);
- // 获取结果集...
- } catch (SQLException e) {
- e.printStackTrace();
- }
- return students;
- }
在 Servlet 中获取分页参数并使首页显示的 StudentList 用 page 的参数来获取:
- // 获取分页参数
- int start = 0;
- int count = 10;
- try {
- start = Integer.parseInt(req.getParameter("page.start"));
- count = Integer.parseInt(req.getParameter("page.count"));
- } catch (Exception e) {
- }
- Page page = new Page(start, count);
- List<Student> students = studentDAO.list(page.getStart(), page.getCount());
- ....
- // 共享数据
- req.setAttribute("page", page);
- req.setAttribute("students", students);
以上即可完成分页功能, 但这是基于 Servlet 的版本, 在之前写过的项目 (学生管理系统(简易版) https://www.jianshu.com/p/553fc76bb8eb ) 中实际的使用了这种方法, 感兴趣的可以去看一下.
SSM 中的分页
在 SSM 项目中, 我们可以使用 MyBatis 的一款分页插件: PageHelper 来帮助我们更加简单的完成分页的需求, 官网在这里: https://github.com/pagehelper/Mybatis-PageHelper
在这里, 我们演示一下如何使用上面的工具重构我们之前写过的 SSM 项目 -- 学生管理系统 - SSM 版 https://www.jianshu.com/p/6a594fbea51d
第一步: 添加相关 jar 依赖包
PageHelper 需要依赖两个 jar 包, 我们直接在 pom.xml 中增加两个 jar 包依赖:
<!-- pageHelper -->
- <dependency>
- <groupId>com.github.pagehelper</groupId>
- <artifactId>pagehelper</artifactId>
- <version>5.1.2-beta</version>
- </dependency>
- <!--jsqlparser-->
- <dependency>
- <groupId>com.github.jsqlparser</groupId>
- <artifactId>jsqlparser</artifactId>
- <version>1.0</version>
- </dependency>
第二步: 配置相关环境
在 MyBatis 的 SessionFactory 配置中新增加一个属性名 plugins 的配置:
<!-- 配置 SqlSessionFactory 对象 -->
- <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
- <!-- 注入数据库连接池 -->
- <property name="dataSource" ref="dataSource"/>
- <!-- 扫描 entity 包 使用别名 -->
- <property name="typeAliasesPackage" value="cn.wmyskxz.entity"/>
- <!-- 扫描 sql 配置文件: mapper 需要的 xml 文件 -->
- <property name="mapperLocations" value="classpath:mapper/*.xml"/>
- <!-- 让 MyBatis 支持 PageHelper 插件 -->
- <property name="plugins">
- <array>
- <bean class="com.github.pagehelper.PageInterceptor">
- <property name="properties">
- <!-- 使用下面的方式配置参数, 一行配置一个 -->
- <value>
- </value>
- </property>
- </bean>
- </array>
- </property>
- </bean>
第三步: 重构项目
首先我们把 LIMIT 关键字从映射文件中干掉:
<!-- 查询从 start 位置开始的 count 条数据 -->
<select id="list" resultMap="student">
SELECT * FROM student ORDER BY student_id desc
</select>
然后注释掉查询数据总条数的 SQL 语句:
- <!--<!– 查询数据条目 –>-->
- <!--<select id="getTotal" resultType="int">-->
- <!--SELECT COUNT(*) FROM student-->
- <!--</select>-->
在 Dao 类和 Service 类中修改相应的地方:
然后修改掉 StudentController 中的方法:
- @RequestMapping("/listStudent")
- public String listStudent(HttpServletRequest request, HttpServletResponse response) {
- // 获取分页参数
- int start = 0;
- int count = 10;
- try {
- start = Integer.parseInt(request.getParameter("page.start"));
- count = Integer.parseInt(request.getParameter("page.count"));
- } catch (Exception e) {
- }
- Page page = new Page(start, count);
- // 使用 PageHelper 来设置分页
- PageHelper.offsetPage(page.getStart(),page.getCount());
- List<Student> students = studentService.list();
- // 使用 PageHelper 来获取总数
- int total = (int) new PageInfo<>(students).getTotal();
- page.setTotal(total);
- request.setAttribute("students", students);
- request.setAttribute("page", page);
- return "listStudent";
- }
重启服务器, 能看到也能够正确的使用分页功能.
总结
其实我自己对于这个工具比较无感.. 因为只是弱化了少一部分的功能, 并没有我想象中的那样 "智能" , 也没有看到什么好的博文能够点通我的认知, 希望了解的大大们能无私分享一下, 谢谢!
来源: https://juejin.im/entry/5ae136546fb9a07aa43be530