Java Web -【分页功能】详解

分页简介

分页功能在网页中是非常常见的一个功能,其作用也就是将数据分割成多个页面来进行显示。

  • 使用场景: 当取到的数据量达到一定的时候,就需要使用分页来进行数据分割。

当我们不使用分页功能的时候,会面临许多的问题:

  • 客户端的问题: 如果数据量太多,都显示在同一个页面的话,会因为页面太长严重影响到用户的体验,也不便于操作,也会出现加载太慢的问题。
  • 服务端的问题: 如果数据量太多,可能会造成内存溢出,而且一次请求携带的数据太多,对服务器的性能也是一个考验。

分页的分类

分页的实现分为真分页和假分页两种,也就是物理分页和逻辑分页。

1.真分页(物理分页):

  • 实现原理: SELECT * FROM xxx [WHERE...] LIMIT #{param1}, #{param2} 第一个参数是开始数据的索引位置 第二个参数是要查询多少条数据
  • 优点: 不会造成内存溢出
  • 缺点: 翻页的速度比较慢

2.假分页(逻辑分页):

  • 实现原理: 一次性将所有的数据查询出来放在内存之中,每次需要查询的时候就直接从内存之中去取出相应索引区间的数据
  • 优点: 分页的速度比较快
  • 缺点: 可能造成内存溢出

传统的分页方式

对于假分页的实现方式很简单,只需要准备一个集合保存从数据库中取出的所有数据,然后根据当前页面的码数,取出对应范围的数据显示就好了,我们这里基于物理分页来实现。

分页的原理

  • 页面中的数据有: 结果集:通过 SQL 语句查询得来的——List<Student>
  • 分页条中的数据有: 当前页:用户传递到后台——currentPage 总页数:计算的来——totalPage 上一页:计算的来——prePage 下一页:计算的来——nextPage 尾页:计算的来(总页数)——lastPage 页面大小(即每一页显示的条数):用户传递到后台——count 总条数:通过 SQL 语句查询得来的——totalCount

可以发现页面功能中需要用到的数据有两个是需要通过 SQL 语句查询得来的:一个是页面中显示的数据 List<Student> ,另一个是数据的总条数 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 实现一个分页条的简单例子,如果不熟悉的童鞋可以去菜鸟教程中查看:点这里


简单版本的分页条

为了便于理解,我们先来实现一个简单版本的分页条吧:

  • 首页超链:指向了 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 - 1varStatus 相当于是循环变量

  • 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 的版本,在之前写过的项目(学生管理系统(简易版))中实际的使用了这种方法,感兴趣的可以去看一下。


SSM 中的分页

在 SSM 项目中,我们可以使用 MyBatis 的一款分页插件: PageHelper 来帮助我们更加简单的完成分页的需求,官网在这里: PageHelper

在这里,我们演示一下如何使用上面的工具重构我们之前写过的 SSM 项目 —— 学生管理系统-SSM 版

第一步:添加相关 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 语句:

<!--&lt;!&ndash; 查询数据条目 &ndash;&gt;-->
<!--<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";
}

重启服务器,能看到也能够正确的使用分页功能。

总结

其实我自己对于这个工具比较无感..因为只是弱化了少一部分的功能,并没有我想象中的那样 “智能” ,也没有看到什么好的博文能够点通我的认知,希望了解的大大们能无私分享一下,谢谢!

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏决胜机器学习

设计模式专题(二十) ——职责链模式

设计模式专题(二十)——职责链模式 (原创内容,转载请注明来源,谢谢) 一、概述 职责链模式(Chainof Responsibility),是使多个对象都有...

35790
来自专栏小狼的世界

locale的详细解释

* Thu Sep 27 2001 Bernhard Rosenkraenzer 2.5-0.f.2

10040
来自专栏Java帮帮-微信公众号-技术文章全总结

Web-第二十二天 Web商城实战二【悟空教程】

`cname` varchar(20) DEFAULT NULL, #分类名称

26840
来自专栏逢魔安全实验室

SQL注入ByPass的一些小技巧

? 01 — 前言 SQL注入从古至今都是一个经久不衰的影响严重的高危漏洞,但是网络安全发展到现在,如果想通过SQL注入直接获取数据或者权限,多多少少都需要绕...

44090
来自专栏深度学习之tensorflow实战篇

mongodb11天之屠龙宝刀(四)高级查询:MongoDB内嵌字段查询

mongodb11天之获取屠龙宝刀(四)高级查询:MongoDB内嵌字段查询 实战环境 IDE:nosql manager for mongodb 表...

32940
来自专栏更流畅、简洁的软件开发方式

【测试】两种数据库,四种分页算法的效率比较

分页算法本身没有什么快慢之分,对反应速度起到决定作用的是——能否有效地利用索引! 算法 评价 缺点 适用的数据库 max 效率最高的 只能有一...

31870
来自专栏更流畅、简洁的软件开发方式

分页解决方案 之 分页算法——Pager_SQL的思路和使用方法

      分页算法(也就是分页读取数据的时候使用的select 语句)面临两大难题:一个是不同的数据库使用的分页算法是不一样的(比如SQL Server 20...

27880
来自专栏Java帮帮-微信公众号-技术文章全总结

第二十八天 管家婆家庭记账软件【悟空教程】

本项目为JAVAEE基础班综合项目,包含了若干个知识点,达到将基础班所学知识综合使用,提高了我们对项目的理解与知识点的运用。

35150
来自专栏更流畅、简洁的软件开发方式

翻动100万级的数据 —— 只需几十毫秒

感谢大家的支持!!! 昨天发了一个邀请,邀请大家帮忙测试,效果还可以,下面小结一下: 通过内部的计数器得知:访问次数是1071(其中有好多是自己点的:)),...

31950
来自专栏精讲JAVA

Java并发:隐藏的线程死锁

许多程序员都熟悉Java线程死锁的概念。死锁就是两个线程一直相互等待。这种情况通常是由同步或者锁的访问(读或写)不当造成的。

12330

扫码关注云+社区

领取腾讯云代金券