分页其实随处可见,例如说最常用的百度搜索后的结果显示,亦或者说论坛中的显示帖子列表。分页实现的方式有很多种,在这一章里主要讲的是用SQL语句结合Servlet来实现分页效果。
目录
分页
作用
分页的作用其实很简单,就是将数据分割成多个页面来进行显示。若数据量过于庞大,在一个页面内全部显示,第一会影响到用户的体验,第二会加重服务器的承载负担。
组成
每一个页面中需要包含哪一些才能实现分页的效果呢?在这里列出需要的数据:
'''
1.要显示的数据结果集:beanList,通过查询数据库得来。
2.当前页:currentPage,通过页面传递到后台,默认第一页
3.总页数:totalPage,通过后台计算得来
4.总记录数:totalRecord,通过查询数据库得来
5.每页记录数:pageSize,即每一页显示的记录条数
6.请求参数:url
'''
通过上面列举的发现,其实只有两个数据是从数据库中获取,其余的都是通过计算而来。那么需要计算的数据所用到的公式如下:
总页数:
totalPage = 总条数 % 每页记录数 == 0? 总条数/每页记录数 : 总条数/每页记录数+1
例子
配置
这里以学生信息为例,页面从简,只分了五个:index.jsp、welcome.jsp、top.jsp、list.jsp、query.jsp。
需要导入的包如下所示:
首先在MYSQL创建一个名为school的数据库,数据库内再新建一个学生表tb_student,SQL语句如下所示:
CREATE DATABASE IF NOT EXISTS school;
use school;
CREATE TABLE tb_student (
sid char(32) PRIMARY KEY,
sname varchar(30) NOT NULL,
gender varchar(6) NOT NULL,
age int(11) NOT NULL,
cellphone varchar(15) NOT NULL,
email varchar(40) NOT NULL
);
接着配置项目中各层的基本分工,如下所示:
其中,domain层表示相关的业务数据类,dao层表示对数据库的操作,service层则是业务逻辑层,servlet层则为处理请求的地方。
分别在domain、dao、service、servlet层创建相关的类文件,如下所示:
domain:
public class Student {
private String sid;
private String sname;
private String gender;
private int age;
private String cellphone;
private String email;
public String getSid() {
return sid;
}
public void setSid(String sid) {
this.sid = sid;
}
public String getSname() {
return sname;
}
public void setSname(String sname) {
this.sname = sname;
}
public String getGender() {
return gender;
}
public void setGender(String gender) {
this.gender = gender;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getCellphone() {
return cellphone;
}
public void setCellphone(String cellphone) {
this.cellphone = cellphone;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
}
dao:
public class StudentDao {
private QueryRunner qr = new ExQueryRunner();
}
service:
public class StudentService {
private StudentDao studentDao = new StudentDao();
}
servlet:
在早期中一篇文章中曾经提及过一个BaseServlet,即能够处理多个请求方法的一个Servlet(链接:)。在这里就得令StudentServlet继承这个BaseServlet。因此,StudentServlet代码如下所示:
public class StudentServlet extends BaseServlet {
private StudentService studentService = new StudentService();
}
在这里要用到先前写到的几个小工具,分别如下所示:
CommonUtils:
public class CommonUtils {
public static String uuid() {
return UUID.randomUUID().toString().replace("-", "").toUpperCase();
}
public static <T> T toBean(Map map, Class<T> clazz) {
try {
/*
* 1. 通过参数clazz创建实例
* 2. 使用BeanUtils.populate把map的数据封闭到bean中
*/
T bean = clazz.newInstance();
ConvertUtils.register(new DateConverter(), java.util.Date.class);
BeanUtils.populate(bean, map);
return bean;
} catch(Exception e) {
throw new RuntimeException(e);
}
}
}
JdbcUtils:
public class JdbcUtils {
private static ComboPooledDataSource dataSource = new ComboPooledDataSource();
// 事务专用Connection
private static ThreadLocal<Connection> tl = new ThreadLocal<Connection>();
public static ComboPooledDataSource getDataSource() {
return dataSource;
}
public static Connection getConnection() throws SQLException {
// 保证事务的connection对象相同 不等于null说明已经开启事务
Connection con = tl.get();
if (con != null) return con;
return dataSource.getConnection();
}
public static void beginTransaction() throws SQLException {
Connection con = tl.get();
if(con != null) throw new SQLException("事务已开启,不要重复开启");
con = getConnection();
con.setAutoCommit(false);
tl.set(con);
}
public static void commitTransaction() throws SQLException {
Connection con = tl.get();
if(con == null) throw new SQLException("还未开启事务,无法提交");
con.commit();
con.close();
/*
* close方法在这里是关闭连接
* 如果不设置connection为null的话,在想使用事务时
* 调用getConnection方法则依旧返回一个已经关闭的连接
* 设置为null表示事务已经结束
*/
tl.remove();
}
public static void rollbackTransaction() throws SQLException {
Connection con = tl.get();
if(con == null) throw new SQLException("还未开启事务,无法回滚");
con.rollback();
con.close();
tl.remove();
}
public static void releaseConnection(Connection connection) throws SQLException {
Connection con = tl.get();
// 如果con为null 说明此时没有事务 关闭
if(con == null) connection.close();
// 如果con不为null 说明有事务,那么需要判断参数连接是否与con相等
if(con != connection) connection.close();
}
}
ExQueryRunner:
public class ExQueryRunner extends QueryRunner {
@Override
public int[] batch(String sql, Object[][] params) throws SQLException {
Connection con = JdbcUtils02.getConnection();
int[] result = super.batch(con, sql, params);
JdbcUtils02.releaseConnection(con);
return result;
}
@Override
public <T> T query(String sql, ResultSetHandler<T> rsh, Object... params) throws SQLException {
Connection con = JdbcUtils02.getConnection();
T result = super.query(con, sql, rsh, params);
JdbcUtils02.releaseConnection(con);
return result;
}
@Override
public <T> T query(String sql, ResultSetHandler<T> rsh) throws SQLException {
Connection con = JdbcUtils02.getConnection();
T result = super.query(con, sql, rsh);
JdbcUtils02.releaseConnection(con);
return result;
}
@Override
public int update(String sql) throws SQLException {
Connection con = JdbcUtils02.getConnection();
int result = super.update(con, sql);
JdbcUtils02.releaseConnection(con);
return result;
}
@Override
public int update(String sql, Object param) throws SQLException {
Connection con = JdbcUtils02.getConnection();
int result = super.update(con, sql, param);
JdbcUtils02.releaseConnection(con);
return result;
}
@Override
public int update(String sql, Object... params) throws SQLException {
Connection con = JdbcUtils02.getConnection();
int result = super.update(con, sql, params);
JdbcUtils02.releaseConnection(con);
return result;
}
}
在配置完c3p0的各项属性后,最后批量插入200个学生信息,如下所示:
public void insert() throws SQLException {
String sql = "insert into tb_student values(?, ?, ?, ?, ?, ?)";
for (int i = 0; i < 200; i++) {
Object[] params = { CommonUtils.uuid(), "stu" + i, i % 2 == 0 ? "男" : "女",
(int) (5 * Math.random() + 18),"15813554755",
"stu" + i + "@aa.com"
};
qr.update(sql, params); }
}
配置完基本的东西后,现在开始做分页的处理。
实现
首先先实现所有数据的显示,那么就得先理清一下思路:
点击【查看学生】,请求StudentServlet查询数据库,然后返回一个List<Student>对象,将这个对象保存至Request域当中请求转发到list.jsp。
根据这个思路,先在StudentServlet中写出如下代码:
public String findAll(HttpServletRequest request, HttpServletResponse response) {
List<Student> stuList = studentService.findAll();
request.setAttribute("stuList", stuList);
return "f:/list.jsp";
}
接下来分别在StudentService以及StudentDao中写下如下代码:
public List<Student> findAll() {
return studentDao.findAll();
}
public List<Student> findAll() {
try {
String sql = "select * from tb_student";
return qr.query(sql, new BeanListHandler<Student>(Student.class));
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
最后需要到显示的页面list.jsp当中循环遍历出数据。
<table border="1" width="70%" align="center">
<tr>
<th>学生姓名</th>
<th>性别</th>
<th>年龄</th>
<th>手机</th>
<th>邮箱</th>
</tr>
<c:forEach items="${requestScope.stuList}" var="stu">
<tr>
<td>${stu.sname }</td>
<td>${stu.gender }</td>
<td>${stu.age }</td>
<td>${stu.cellphone }</td>
<td>${stu.email }</td>
</tr>
</c:forEach>
</table>
结果如下所示:
接着再来实现所有数据的分页显示,首先将分页需要的数据封装到一个类中,将其命名为pageBean,其代码如下所示:
public class PageBean<T> {
private int cp; // 当前页面currentPage
private int tp; // 总页数totalPage
private int tr; // 总记录数 totalRecord
private int ps; // 每页记录数 pageSize
private List<T> beanList; // 当前页的记录
private String url; //记录请求参数
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public int getCp() {
return cp;
}
public void setCp(int cp) {
this.cp = cp;
}
public int getTp() {
int tp = tr / ps;
return tr%ps==0? tp : tp + 1;
}
public int getTr() {
return tr;
}
public void setTr(int tr) {
this.tr = tr;
}
public int getPs() {
return ps;
}
public void setPs(int ps) {
this.ps = ps;
}
public List<T> getBeanList() {
return beanList;
}
public void setBeanList(List<T> beanList) {
this.beanList = beanList;
}
}
这里就有几个问题:第一、当前页的页数由谁传递;第二、一页中显示多少条数据在哪里设置;第三、Dao中如何实现返回的是一页的数据。
第一个问题的解决:当前页的页数由当前显示的页面传递给Servlet,若没有则默认为1;第二个问题的解决:在Servlet中设置;第三个问题的解决:通过SQL语句中的limit字句来实现返回的一页数据。limit在sql中是限制select查询返回的记录行数。
想清楚了这几个问题,那么接下来先修改StudentServlet:
public String findAll(HttpServletRequest request, HttpServletResponse response) {
/*
* 1. 获取当前页码
* 2. 设置一页显示多少记录
* 3. 传递两个参数到dao中查询数据库
* 4. 将返回的PageBean对象保存至Request域中
* */
int cp = getCp(request);
int ps = 10;
PageBean<Student> pb = studentService.findAll(cp, ps);
request.setAttribute("pb", pb);
return "f:/list.jsp";
}
接着再来修改StudentDao中的sql语句以及返回的对象。
public PageBean<Student> findAll(int cp, int ps) {
try {
//创建PageBean对象并且保存当前页码以及一页显示的记录数
PageBean<Student> pb = new PageBean<Student>();
pb.setCp(cp);
pb.setPs(ps);
//设置总记录数,从数据库当中获取
String sql = "select count(*) from tb_student";
Number num = (Number) qr.query(sql, new ScalarHandler());
pb.setTr(num.intValue());
sql = "select * from tb_student limit ?, ?";
Object[] params = {(cp-1)*ps, ps};
pb.setBeanList(qr.query(sql, new BeanListHandler<Student>(Student.class), params));
//设置完四大属性返回PageBean对象
return pb;
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
最后修改list.jsp当中的显示,这里就有一个问题:如何动态地显示分页页码列表?其实很简单,只需要通过当前页的页码来定出页码列表。因此,我们只需要通过当前页页码currentPage来计算页码的开头begin和页码的末尾end。这里就需要有一些公式来达到这个效果,如下所示:
'''
前提:最多显示10个页码、当前页在页码列表的位置为6
如果总页数<=10(列表长度),那么begin=1, end = 总页数;
否则使用公式计算:begin = pc - 5;end = pc + 4;
假设头溢出:当begin < 1时,令begin = 1;
假设尾溢出:当begin > 总页数时, 令end = 总页数
'''
根据这个公式以及上述的思路,list.jsp的完整代码如下所示:
<center>
第${pb.cp }页/共${pb.tp }页
<a href="<c:url value='/StudentServlet?method=findAll'/>&cp=1">首页</a>
<c:if test="${pb.cp > 1 }">
<a href="<c:url value='/StudentServlet?method=findAll'/>&cp=${pb.cp-1 }">上一页</a>
</c:if>
<c:choose>
<c:when test="${pb.tp <= 10 }">
<c:set var="begin" value = "1" />
<c:set var="end" value = "${pb.tp }" />
</c:when>
<c:otherwise>
<c:set var="begin" value = "${pb.cp - 5 }" />
<c:set var="end" value = "${pb.cp + 4 }" />
<c:if test="${begin < 1 }">
<c:set var="begin" value = "1" />
<c:set var="end" value = "10" />
</c:if>
<c:if test="${end > pb.tp }">
<c:set var="begin" value = "${pb.tp - 9 }" />
<c:set var="end" value = "${pb.tp }" />
</c:if>
</c:otherwise>
</c:choose>
<c:forEach var="i" begin="${begin }" end="${end }">
<c:choose>
<c:when test="${i eq pb.cp }">
[${i }]
</c:when>
<c:otherwise>
<a href="<c:url value='/StudentServlet?method=findAll'/>&cp=${i }">[${i }]</a>
</c:otherwise>
</c:choose>
</c:forEach>
<c:if test="${pb.cp < pb.tp }">
<a href="<c:url value='/StudentServlet?method=findAll'/>&cp=${pb.cp+1 }">上一页</a>
</c:if>
<a href="<c:url value='/StudentServlet?method=findAll'/>&cp=${pb.tp }">尾页</a>
</center>
结果呈现:
最基本的全部数据分页显示就此完成。接下来需要通过查询搜索来显示分页,这里先抛出一个问题以供思考:输入的查询条件如何保存,以至于第二页甚至后面的页码里显示的都是符合条件的数据?
先理清一个思路:点击【查询】按钮,将输入的表单数据传递到Servlet,通过StudentDao查询符合条件的记录,并将所有符合条件的记录封装到一个List中并保存至pageBean内,返回这个pageBean对象,最后将这个pageBean对象保存至Request域当中转发到list.jsp。
那么到底如何保存这个查询条件呢?通过将表单的上传方式改成get即可完成这样的效果,即在url中记录请求参数。这就是pageBean当中的最后一个属性url的意义,也即分页中组成里的url的意义。
根据这个思路,先修改StudentServlet:
// 截取url : 项目名/Servlet路径?参数字符串
public String getUrl(HttpServletRequest request) {
String contextPath = request.getContextPath();
String servletPath = request.getServletPath();
String queryString = request.getQueryString();
if(queryString.contains("&cp=")) {
int index = queryString.lastIndexOf("&cp=");
queryString = queryString.substring(0, index);
}
return contextPath + servletPath + "?" + queryString;
}
// 获取当前页码
public int getCp(HttpServletRequest request) {
String value = request.getParameter("cp");
if (value == null || value.trim().isEmpty())
return 1;
return Integer.parseInt(value);
}
private Student encoding(Student stu) throws UnsupportedEncodingException {
String sname = stu.getSname();
String gender = stu.getGender();
String cellphone = stu.getCellphone();
String email = stu.getEmail();
if(sname != null && !sname.trim().isEmpty()) {
sname = new String(sname.getBytes("ISO-8859-1"), "UTF-8");
stu.setSname(sname);
}
if(gender != null && !gender.trim().isEmpty()) {
gender = new String(gender.getBytes("ISO-8859-1"), "UTF-8");
stu.setGender(gender);
}
if(cellphone != null && !cellphone.trim().isEmpty()) {
cellphone = new String(cellphone.getBytes("ISO-8859-1"), "UTF-8");
stu.setCellphone(cellphone);
}
if(email != null && !email.trim().isEmpty()) {
email = new String(email.getBytes("ISO-8859-1"), "UTF-8");
stu.setEmail(email);
}
return stu;
}
public String query(HttpServletRequest request, HttpServletResponse response) throws UnsupportedEncodingException {
// 将表单内的所有参数封装至Student内
Student criteria = CommonUtils.toBean(request.getParameterMap(), Student.class);
// 由于是get请求 需要处理乱码问题
criteria = encoding(criteria);
int cp = getCp(request);
int ps = 10;
PageBean<Student> pb = studentService.query(criteria, cp, ps);
pb.setUrl(getUrl(request));
request.setAttribute("pb", pb);
return "f:/list.jsp";
}
再修改StudentDao:
public PageBean<Student> query(Student criteria, int cp, int ps) {
try {
PageBean<Student> pb = new PageBean<Student>();
pb.setCp(cp);
pb.setPs(ps);
/*
* 条件查询并非全满足查询,只要满足一个即可
* 除了性别以外的查询都可以是模糊查询
* 除了获取总记录数tr以外,查询记录也需要用到where判断子句
* 因此,将where子句单独拿出来以重用
* */
// 保存参数用
List<Object> params = new ArrayList<Object>();
StringBuilder sql = new StringBuilder("select count(*) from tb_student");
StringBuilder whereSql = new StringBuilder(" where 1 = 1");
String sname = criteria.getSname();
if(sname != null && !sname.trim().isEmpty()) {
whereSql.append(" and sname like ?");
params.add("%" + sname + "%");
}
String gender = criteria.getGender();
if(gender != null && !gender.trim().isEmpty()) {
whereSql.append(" and gender = ?");
params.add(gender);
}
String phone = criteria.getCellphone();
if(phone != null && !phone.trim().isEmpty()) {
whereSql.append(" and cellphone like ?");
params.add("%" + phone + "%");
}
String email = criteria.getEmail();
if(email != null && !email.trim().isEmpty()) {
whereSql.append(" and email like ?");
params.add("%" + email + "%");
}
Number num = (Number) qr.query(sql.append(whereSql).toString(),
new ScalarHandler(), params.toArray());
pb.setTr(num.intValue());
sql = new StringBuilder("select * from tb_student");
StringBuilder limitSql = new StringBuilder(" limit ?, ?");
params.add((cp-1)*ps);
params.add(ps);
pb.setBeanList(qr.query(sql.append(whereSql).append(limitSql).toString(),
new BeanListHandler<Student>(Student.class),
params.toArray()));
return pb;
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
最后修改list.jsp页面:
<a href="${pb.url }&cp=1">首页</a>
<a href="${pb.url }&cp=${pb.cp-1 }">上一页</a>
<a href="${pb.url }&cp=${i }">[${i }]</a>
<a href="${pb.url }&cp=${pb.cp+1 }">上一页</a>
<a href="${pb.url }&cp=${pb.tp }">尾页</a>
只查询为性别为男的学生,结果显示如下:
后话
'''
分页暂时只讲通过SQL中的limit子句完成的部分,除此之外还有过滤器以及框架直接实现分页效果。
下一章讲监听器和过滤器的内容
'''