paging_1

这篇讲 分页算法及其通过 Java 的纯后端实现。

原理

平时在使用Google搜索时,我们可以注意到页底的分页条:

google_paging_bar

它会根据 搜索结果的总数据条数,和 每页要显示的数据条数,自动计算 总页数。且 当前页码 高亮。
在MySQL数据库底层进行分页查询时,使用的是limit,那必须封装一个数据表示 当前页第一条实体数据在数据库中的位置

当我们向后翻页时,根据 分页条的宽度,随着当前页面的变化,分页条会有“滚动”一样的变化:

google_paging_bar2

这里需要封装 分页条起始页码分页条结束页码 来进行计算。

综上所述,要实现分页功能,需要以下几个数据:

  • 当前页的实体数据列表:list
  • 实体数据总记录数:recordCount
  • 当前页页码:currentPage
  • 每页实体数据个数:pageSize
  • 分页条宽度:width
  • 当前页在数据库表中的起始位置:offset
  • 总页数:pageCount
  • 分页条起始页码:begin
  • 分页条结束页码:end

其中:

  • list、recordCount、currentPage 是外界传递进来的,list 和 recordCount 是数据库查询出来的,而 currentPage 是用户选择而来的;
  • pageSize、width 是一开始就给定默认值的;
  • offset、pageCount、begin、end 需要通过计算得出。

推演

1.计算当前页在数据库表中的起始位置(offset)

数据库标示数据记录的索引是从0开始,
所以,假设每页实体数据个数是3,那么,第1页的起始位置是0,第2页的起始位置则是3,第3页的起始位置则是6,其他的如此类推下去。

当有了 当前页页码(currentpage)每页实体数据个数(pageSize)这两个值时,就可以计算这个数据变量了。

计算公式:$offset = (currentPage - 1) * pageSize$

2.计算总页数(pageCount)

当有了实体数据总记录数(recordCount)和每页实体数据个数(pageSize)这两个数值,那就可以计算总页数(pageCount)了。

计算公式:$pageCount = recordCount / pageSize + (recordCount % pageSize == 0 ? 0 : 1)$

3.计算起始页码(begin)与结束页码(end)

首先要明确的是,起始页码和结束页码的变化与 当前页页码总页数分页条宽度 这3个数据变量有关。

分页条的变化,大体分为2种情况:

  1. 实体数据不多,总页数少于等于分页条宽度。那么分页条的起始页码就是1,而结束页码就是总页数。

  2. 总页数大于分页条宽度。这种情况里面又细分了几种情况,事情就变得复杂许多了。

下面具体分析第二种情况。

先给定总页数为15,分页条宽度为 10,作图分析:

paing_1

可以看出,分页条的变化分为3种情况:

  1. 当前页页码小于等于分页条宽度一半
  2. 当前页页码大于总页数减去分页条宽度一半
  3. 当前页页码大于分页条宽度一半并且小于等于总页数减去分页条宽度一半

再给定分页条宽度为 5,则分页条宽度为奇数时,分析如下:

paging_2

实现

从底层开始开发,先把数据库以及对应的表构建起来:

1
2
3
4
5
6
7
8
9
10
11
12
CREATE DATABASE paging
CHARACTER SET utf8
COLLATE utf8_general_ci;
USE paging;
CREATE TABLE book(
id VARCHAR(255) PRIMARY KEY,
name VARCHAR(60) NOT NULL UNIQUE,
author VARCHAR(20) NOT NULL,
price DECIMAL(8, 2),
publisher VARCHAR(255),
type VARCHAR(30)
);

然后,可以根据表来创建对应的类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class Book {
private String id;
private String name;
private String author;
private double price;
private String publisher;
private String type;
public Book() {
}
public Book(String id, String name, String author, double price,
String publisher, String type) {
this.id = id;
this.name = name;
this.author = author;
this.price = price;
this.publisher = publisher;
this.type = type;
}
@Override
public String toString() {
return "Book [id=" + id + ", name=" + name + ", author=" + author
+ ", price=" + price + ", publisher=" + publisher + ", type="
+ type + "]";
}
// 省略get/set方法
}

接着,则要来构建并实现封装分页数据的分页类,它是整个分页功能最核心的敌方。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
public class Page<T> {
// 一页的数据
private List<T> list = null;
// 一页数据的起始位置
private int offset = 0;
// 一页数据的大小
private int pageSize = 2;
// 总记录数
private int recordCount = 0;
// 总页数
private int pageCount = 0;
//当前页索引
private int currentPage = 0;
// 分页条宽度
private int width = 10;
// 分页条起始位置
private int begin;
// 分页条结束位置
private int end;
public Page(int rCount, int currPage) {
recordCount = rCount;
currentPage = currPage;
pageCount = recordCount / pageSize + (recordCount % pageSize == 0 ? 0 : 1);
offset = (currentPage - 1) * pageSize;
// 计算分页条起始和结束的位置
// 具体分为两种情况:
// 1.总页数小于等于分页条宽度
// 2.总页数大于分页条宽度
if (pageCount <= width) {
begin = 1;
end = pageCount;
} else {
// 当总页数多于分页条宽度时, 又分3种情况
// 1.当前页索引小于等于分页条一半宽度
// 2.当前页索引大于(总页数-分页条一半宽度)
// 3.当前页索引介于条件1, 2之间
int pivot = width / 2;
if (currentPage <= pivot) {
begin = 1;
end = width;
} else if (currentPage > pageCount - pivot) {
begin = pageCount - width + 1;
end = pageCount;
} else {
begin = currentPage - pivot;
// width & 0x1 ^ 0x1的作用相当于width % 2 == 0 : 1 : 0
end = currentPage + pivot - (width & 0x1 ^ 0x1);
}
}
}
public List<T> getList() {
return list;
}
public void setList(List<T> list) {
this.list = list;
}
public int getOffset() {
return offset;
}
public int getPageSize() {
return pageSize;
}
public void setPageSize(int pageSize) {
this.pageSize = pageSize;
}
public int getRecordCount() {
return recordCount;
}
public void setRecordCount(int recordCount) {
this.recordCount = recordCount;
}
public int getPageCount() {
return pageCount;
}
public void setPageCount(int pageCount) {
this.pageCount = pageCount;
}
public int getCurrentPage() {
return currentPage;
}
public void setCurrentPage(int currentPage) {
this.currentPage = currentPage;
}
public int getWidth() {
return width;
}
public void setWidth(int width) {
this.width = width;
}
public int getBegin() {
return begin;
}
public void setBegin(int begin) {
this.begin = begin;
}
public int getEnd() {
return end;
}
public void setEnd(int end) {
this.end = end;
}
}

在Page类的构造函数里运用了之前推导的计算公式,将每个分页要用到的数据变量都计算出来。

接下来,我们只需要往数据库表里面插入一些数据,在查询出来显示到页面上即可。

操作图书数据的Dao类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
public class BookDao {
/**
* 获取一页图书数据
*
* @param offset
* @param size
* @return
*/
public List<Book> getPageData(int offset, int size) {
List<Book> list = new ArrayList<Book>();
Connection con = null;
PreparedStatement pStmt = null;
ResultSet rs = null;
try {
con = DBUtils.getConnection();
String sql = "select * from book limit ?, ?";
pStmt = con.prepareStatement(sql);
// 设置参数
pStmt.setInt(1, offset);
pStmt.setInt(2, size);
// 执行查询
rs = pStmt.executeQuery();
while (rs.next()) {
String id = rs.getString(1);
String name = rs.getString(2);
String author = rs.getString(3);
double price = rs.getDouble(4);
String publisher = rs.getString(5);
String type = rs.getString(6);
Book book = new Book(id, name, author, price, publisher, type);
list.add(book);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
DBUtils.release(con, pStmt, rs);
}
return list;
}
/**
* 获取统计的图书记录数
*
* @return
*/
public int getCount() {
int count = 0;
Connection con = null;
Statement stmt = null;
ResultSet rs = null;
try {
con = DBUtils.getConnection();
stmt = con.createStatement();
String sql = "select count(*) from book";
rs = stmt.executeQuery(sql);
rs.next();
count = rs.getInt(1);
} catch (Exception e) {
e.printStackTrace();
} finally {
DBUtils.release(con, stmt, rs);
}
return count;
}
/**
* 插入图书数据
*
* @param book
*/
public void insert(Book book) {
Connection con = null;
PreparedStatement pStmt = null;
try {
con = DBUtils.getConnection();
String sql = "insert into book values(?, ?, ?, ?, ?, ?)";
pStmt = con.prepareStatement(sql);
// 设置参数
pStmt.setString(1, book.getId());
pStmt.setString(2, book.getName());
pStmt.setString(3, book.getAuthor());
pStmt.setDouble(4, book.getPrice());
pStmt.setString(5, book.getPublisher());
pStmt.setString(6, book.getType());
// 执行
pStmt.execute();
} catch (Exception e) {
e.printStackTrace();
} finally {
DBUtils.release(con, pStmt, null);
}
}
/**
* 删除所有图书数据
*/
public void deleteAll() {
Connection con = null;
Statement stmt = null;
try {
con = DBUtils.getConnection();
stmt = con.createStatement();
String sql = "delete from book";
stmt.execute(sql);
} catch (Exception e) {
e.printStackTrace();
} finally {
DBUtils.release(con, stmt, null);
}
}
}

写一个测试方法,随便插入一些图书数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
public void testInsert() throws Exception {
BookDao bookDao = new BookDao();
for (int i = 0; i < 50; i++) {
Book book = new Book(UUID.randomUUID().toString(),
"Thinking In Java" + i,
"makwan" + i,
60.0 + i,
"机械工业出版社" + i,
"Java编程语言");
bookDao.insert(book);
}
}

处理图书数据的服务层:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class BookService {
private static BookDao bookDao = new BookDao();
public Page<Book> getPageData(String currentPage) {
Page<Book> page = null;
try {
// 获取总记录数
int count = bookDao.getCount();
if (count > 0) {
int cp = StringUtils.isBlank(currentPage) ? 1
: Integer.parseInt(currentPage);
page = new Page<Book>(count, cp);
// 获取指定页的数据
List<Book> list = bookDao.getPageData(page.getOffset(), page.getPageSize());
page.setList(list);
}
} catch (Exception e) {
throw new RuntimeException(e);
}
return page;
}
}

用于显示图书分页数据的jsp页面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>图书信息展示页面</title>
<style type="text/css">
.pagingbar {
text-align: center;
}
a {
text-decoration: none;
}
</style>
</head>
<body>
<!-- 数据列表 -->
<table align="center" border="1">
<thead>
<tr>
<th>编号</th>
<th>书名</th>
<th>作者</th>
<th>价格</th>
<th>出版社</th>
<th>类型</th>
</tr>
</thead>
<tbody>
<c:choose>
<c:when test="${not empty requestScope.page}">
<c:forEach items="${requestScope.page.list}" var="book">
<tr>
<td>${book.id}</td>
<td>${book.name}</td>
<td>${book.author}</td>
<td>${book.price}</td>
<td>${book.publisher}</td>
<td>${book.type}</td>
</tr>
</c:forEach>
</c:when>
<c:otherwise>
<tr>
<td colspan="9">
<font color="red">当前没有数据</font>
</td>
</tr>
</c:otherwise>
</c:choose>
</tbody>
</table>
<!-- 分页条 -->
<div class="pagingbar">
<c:if test="${not empty requestScope.page}">
<c:if test="${requestScope.page.currentPage > 1}">
<a href="${pageContext.request.contextPath}/bookservlet?method=getPageData&currentPage=1">首页</a>
</c:if>
<c:if test="${requestScope.page.currentPage > 1}">
<a href="${pageContext.request.contextPath}/bookservlet?method=getPageData&currentPage=${requestScope.page.currentPage - 1}">上一页</a>
</c:if>
<c:forEach begin="${requestScope.page.begin}" end="${requestScope.page.end}" step="1" var="pageNum">
<a href="${pageContext.request.contextPath}/bookservlet?method=getPageData&currentPage=${pageNum}"
${pageNum == requestScope.page.currentPage ? "style='font-size:20px;color:red;'" : "style='color:blue;'"}
>
${pageNum}
</a>
</c:forEach>
<c:if test="${requestScope.page.currentPage < requestScope.page.pageCount}">
<a href="${pageContext.request.contextPath}/bookservlet?method=getPageData&currentPage=${requestScope.page.currentPage + 1}">下一页</a>
</c:if>
<c:if test="${requestScope.page.currentPage < requestScope.page.pageCount}">
<a href="${pageContext.request.contextPath}/bookservlet?method=getPageData&currentPage=${requestScope.page.pageCount}">尾页</a>
</c:if>
</c:if>
<form action="${pageContext.request.contextPath}/bookservlet?method=getPageData" method="post" style="display: inline;">
<input type="text" name="currentPage" style="width: 20px"/>
<input type="submit" value="Go">
</form>
当前 ${requestScope.page.currentPage}页
共 ${requestScope.page.pageCount}页
共 ${requestScope.page.recordCount}条记录
</div>
</body>
</html>