本篇讲诉如何在页面中通过操作数据库来完成数据显示的分页功能。当一个操作数据库进行查询的语句返回的结果集内容如果过多,那么内存极有可能溢出,所以在大数据的情况下分页是必须的。当然分页能通过很多种方式来实现,而这里我们采用的是操作数据库的方式,而且在这种方式中,最重要的是带限制条件的查询SQL语句:
select name from user limit m,n
其中m与n为数字。n代表需要获取多少行的数据项,而m代表从哪开始(以0为起始),例如我们想从user表中先获取前五行数据项(1-5)的name列数据,则SQL为:
select name from user limit 0,5;
那么如果要继续往下看一页五行的数据项(6-10)则下一步的SQL应该为:
select name from user limit 5,5;
再下一页五行的数据项(11-15)的SQL就为:
select name from user limit 15,5;
。。。
如果对上面的SQL语句不熟悉的话,请先查询相关文档再来看本篇内容。
我们先来看看“百度贴吧”中的分页效果:
我选取了首页、次页和尾页三种情况的显示效果,可以看到这个分页显示的效果是比较灵活多变的,开发者可以依据自己的爱好进行展示,但是实质是不变的。
那么接下来我们将做出如下效果的页面显示:
在这个页面中,其实按面向对象的思考方式,这个页面就是一个对象,稍后我们会说到,先来看看在该页面下方的页码分页显示,探究当用户点击之后,分页请求是如何一步步到底层的。
基本流程就如上图所示,那么我们来分析这个过程:
第一步:当用户在页面上点击某一页,或者下一页、上一页等等这些超链接,根据MVC设计模式,这些请求都是交给Servlet处理。
第二步:在Servlet中,首先应该将请求对象带来的信息封装到对象中,由于我们是要查询数据库,因此必须封装成一个查询的条件对象,这里举例为“QueryInfo”自定义对象,在刚对象中包含当前页、每页多少条数据、等会查询数据库从哪开始等信息,只有拥有了这些信息,才能在数据库查询的时候能根据顺序往下翻页。
第三步、第四步:根据web工程的三层设计模式,业务从service层一步步到dao层。
第五步、第六步:通过上层传下来的QueryInfo对象,根据里面封装的信息开始对数据库进行操作,使用select name from user limit startIndex,pageSize 这样的SQL命令,将查询到的结果集返回给dao层。
第七步:dao层根据从数据库返回的结果集,提取出用户想看的页面数据,这里我们将页面数据都封装到一个集合中,除此之外为了之后在页面上能显示页码之类的数据,还必须要获取到查询的总记录数。前面这两个信息数据我们以“QueryResult”自定义对象来封装。
第八步、第九步:在service层,通过dao传递上来的查询结果“QueryResult”对象,和一开始的查询信息“QueryInfo”对象,来构建页面显示信息,例如页面数据、总记录数、总页数、当前页、上一页、下一页、页码条等等,我们将其都封装进“PageBean”这个JavaBean中,对于JSP中要显示的动态数据,我们只需要提取PageBean对象中的属性即可。其实PageBean的对象需要哪些属性,只要看在JSP页面中我们想显示什么数据就行了,设计还是很简单。
第十步、第十一步:service层将页面所需要的信息封装进PageBean对象后,将其传给web层的Servlet,由MVC模式,Servlet再将PageBean对象封装进请求交给JSP来显示。
了解完一个分页功能的实现流程之后,下面我将开始进行分页的实现。
上面的步骤涉及到三个实体对象,分别是QueryInfo,QueryResult、PageBean,而我们在工程中先构建这三个实体,在这三个实体中,有些属性是可以根据别的属性计算出来的,我们没必要提供setter方法。
实体QueryInfo对象:
1 public class QueryInfo {
2 private int currentPage = 1; //用户当前看的页数
3 private int pageSize = 10; //每页多少条显示数据
4 private int startIndex; //记住用户想看的页的数据在数据库的起始位置
5
6 。。。 //此处省略currentPage和pageSize两个属性的set和get方法
7
8 public int getStartIndex() {
9 this.startIndex = (this.currentPage-1)*this.pageSize;
10 return startIndex;
11 }
12 }
注:在查询信息对象QueryInfo中,currentPage和pageSize属性都设置了默认值,如果用户没有特意设置每页显示多少条数据,则根据默认值进行计算。另外由于startIndex属性可以由另外两个属性计算出,因此无需set方法。
实体PageBean对象:
1 public class PageBean {
2 private List contentData; //保存页面数据
3 private int totalRecords; //查询到的总记录数
4 private int currentPage; //用户当前看的页数
5 private int pageSize; //每页多少条显示数据
6 private int totalPages; //总页数
7 private int previousPage; //上一页
8 private int nextPage; //下一页
9 private int[] pageBar; //页码条
10
11 //1,contentData可以从QueryResult对象中获取
12 。。。//此处省略contentData属性的set和get方法
13
14 //2,totalRecords可以从QueryResult对象中获取
15 。。。//此处省略contentData属性的set和get方法
16
17 //3,currentPage可以从QueryInfo对象中获取
18 。。。//此处省略contentData属性的set和get方法
19
20 //4,pageSize可以从QueryInfo对象中获取
21 。。。//此处省略contentData属性的set和get方法
22
23 //5,总页数可以由总页数和页面数据大小这两个属性计算,因此无需set方法
24 public int getTotalPages() {
25 if(totalRecords % pageSize ==0){
26 totalPages = totalRecords / pageSize;
27 }else{
28 totalPages = totalRecords / pageSize + 1;
29 }
30 return totalPages;
31 }
32
33 //6,上一页可以根据当前页计算,因此无需set方法
34 public int getPreviousPage() {
35 if(currentPage == 1) {
36 previousPage = 1;
37 }else {
38 previousPage = currentPage - 1;
39 }
40 return previousPage;
41 }
42
43 //7,下一页可以根据当前页计算,因此无需set方法
44 public int getNextPage() {
45 if(currentPage == totalPages) {
46 nextPage = totalPages;
47 }else {
48 nextPage = currentPage + 1;
49 }
50 return nextPage;
51 }
52
53 //8,页码条可以由总页数来计算显示,因此无需set方法
54 public int[] getPageBar() {
55 pageBar = null;
56 int startIndex ;
57 int endIndex ;
58 if(totalPages<10) {
59 pageBar = new int[totalPages];
60 startIndex = 1;
61 endIndex = totalPages;
62
63 }else{
64 pageBar = new int[10];
65 startIndex = currentPage-5;
66 endIndex = currentPage+4;
67 if(startIndex<1) {
68 startIndex = 1;
69 endIndex = 10;
70 }
71 if(endIndex>totalPages) {
72 startIndex = totalPages-10+1;
73 endIndex = totalPages;
74 }
75 }
76 int index = 0;
77 for(int i=startIndex;i<=endIndex;i++) {
78 pageBar[index] = i;
79 index++;
80 }
81 return pageBar;
82 }}
注:PageBean对象属性会比较多,因为这些属性都是要在页面上显示的内容。虽然属性多,但是由很多属性值可以通过别的属性计算得到,另外的属性可以通过别的对象属性得到。
尤其是页码条pageBar这个属性,这里我的设计是,如果总页数不超过10页的话,那么页码条显示的个数就为总页数;如果总页数超过10页,那么页码条固定显示10个页码,同时如果当前页在最前6个页则页码条保持不变,在中间部分的当前页会保持在页码条的中间位置(前面5个页码,后面4个页码,当前页在第6个位置)。如果当前页到最后部分也是同理。
上面三个对象设计完成后,我们就要来考虑在分页流程中不同层对查询信息的处理方式。
按从下到上的开发流程,首先是dao层,该层必须通过请求发来的查询信息来对数据库进行操作,也就是本文最开始讲解的SQL语句的两个参数是执行数据库操作的关键,本文以显示User用户为分页案例,在数据库中为user表。因此在处理User对象的dao层实现类UserDaoImpl中,查询方法为pageQuery,返回上面刚刚定义的QueryResult对象。
注:该工程是博客《JDBC操作数据库的学习(2)》和《在JDBC中使用PreparedStatement代替Statement,同时预防SQL注入》中工程的扩展,下面使用到JDBC的工具类JdbcUtils即是在《JDBC操作数据库的学习(2)》中的定义。
下面的代码对应流程图中的第五、六、七步骤:
1 package com.fjdingsd.dao.impl;
2 public class UserDaoImpl implements UserDao {
3 public QueryResult pageQuery(int startIndex,int pageSize) {
4 Connection conn = null;
5 PreparedStatement st = null;
6 ResultSet rs = null;
7 QueryResult result = new QueryResult();
8 try{
9 conn = JdbcUtils.getConnection();
10 String sql = "select * from user limit ?,?";
11 st = conn.prepareStatement(sql);
12 st.setInt(1, startIndex);
13 st.setInt(2, pageSize);
14 rs = st.executeQuery();
15 List contentList = new ArrayList();
16 while(rs.next()) {
17 User user = new User();
18 user.setId(rs.getInt("id"));
19 user.setName(rs.getString("name"));
20 user.setAge(rs.getInt("age"));
21 contentList.add(user);
22 }
23 result.setContentData(contentList);
24 //获取了页面数据后还没结束,还得获取总记录数
25 sql = "select count(*) from user";
26 st = conn.prepareStatement(sql);
27 rs = st.executeQuery();
28 if(rs.next()) {
29 int totalRecords = rs.getInt(1); //rs.getInt("count(*)")也是可以的
30 result.setTotalRecords(totalRecords);
31 }
32 return result;
33 }catch (Exception e) {
34 throw new RuntimeException(e);
35 }finally{
36 JdbcUtils.release(conn, st, rs);
37 }
38 }
39 }
上面在dao层对User对象处理的实现类UserDaoImpl已经处理好了分页查询,该pageQuery方法返回的QueryResult对象正是在service层中处理User对象的业务的方法所需要的参数。在service层中,我们需要根据查询得到的结果QueryResult对象,来获取页面显示所需要的对象PageBean。
下面的代码对应流程图的第三、四和第八、九步骤:
1 package com.fjdingsd.service;
2 public class UserServiceImpl {
3 private UserDao userDao = new UserDaoImpl(); //通常使用工程模式获取实现类对象,这里为了简便直接采用实现类的构造器
4
5 public PageBean pageQuery(QueryInfo info) {
6 //获取对应dao的实现类中的查询到的结果数据
7 QueryResult result = userDao.pageQuery(info.getStartIndex(), info.getPageSize());
8
9 //根据dao的查询结果,生成页面显示需要的PageBean
10 PageBean page = new PageBean();
11 page.setContentData(result.getContentData());
12 page.setTotalRecords(result.getTotalRecords());
13 page.setCurrentPage(info.getCurrentPage());
14 page.setPageSize(info.getPageSize());
15
16 return page;
17 }
18 }
上面在service层将查询到的结果对象封装成页面显示所需要的对象PageBean,service层需要将这个对象交给web层的Servlet来处理,其实这个Servlet也是最开始处理请求对象的Servlet,因为最开始要想生成查询信息QueryInfo对象就必须要从请求中提取数据封装。
注:下面代码中使用到了工具类的静态方法WebUtils.request2Bean,是将请求对象中的参数值转移到一个Bean对象中,该方法的实现具体请看《在WEB工程的web层中的编程技巧》。即使Request对象中没有我们需要的参数,那么创建出来的QueryInfo对象中的currentPage和pageSize属性我们在最开始创建时已经设置了默认值,所以无需担心空指针异常。
下面的代码对应流程图中的第一,二和第十、十一步骤:
1 package com.fjdingsd.web.controller;
2 public class UserListServlet extends HttpServlet {
3
4 public void doGet(HttpServletRequest request, HttpServletResponse response)
5 throws ServletException, IOException {
6 try{
7 QueryInfo info = WebUtils.request2Bean(request, QueryInfo.class);
8 UserService userService = new UserServiceImpl();//通常使用工程模式获取实现类对象,这里为了简便直接采用实现类的构造器
9
10 PageBean page = userService.pageQuery(info);
11 request.setAttribute("pagebean", page);
12 request.getRequestDispatcher("/WEB-INF/jsp/userlist.jsp").forward(request, response);
13 }catch (Exception e) {
14 e.printStackTrace();
15 request.setAttribute("message", "查看用户失败");
16 request.getRequestDispatcher("/message.jsp").forward(request, response);
17 }
18 }
19 }
上面在web层中已经使用Servlet将页面需要显示的信息全部封装进PageBean对象中,通过请求对象Request存储,最后转发进相应的JSP页面,这里例子为userlist.jsp页面,最后只要在这个页面中将请求对象中保存的PageBean对象提取出来,再将该对象中的每个属性的内容在页面相应的地方显示即可。
在JSP页面中,我们以表格的形式将页面数据显示出来,除了用户想看的页面数据以外,其他的就是与页码相关的,因为在Servlet中我们将PageBean对象封装进请求Request对象中,所以在JSP页面中我们就可以通过EL表达式将其取出,而是会是大量地使用到EL表达式和JSP标签。
1 <body>
2 <a href="${pageContext.request.contextPath}/servlet/UserListServlet">显示用户</a><br>
3 <table>
4 <tr>
5 <td>用户id</td>
6 <td>用户姓名</td>
7 <td>用户年龄</td>
8 </tr>
9 <c:forEach var="user" items="${requestScope.pagebean.contentData }">
10 <tr>
11 <td>${user.id}</td>
12 <td>${user.name}</td>
13 <td>${user.age}</td>
14 </tr>
15 </c:forEach>
16 </table>
17 <br>
18 <%--
19 共${pagebean.totalRecords} 条记录,每页${pagebean.pageSize}条,共${pagebean.totalPages}页,
20 当前第${pagebean.currentPage}页
21 <a href="${pageContext.request.contextPath}/servlet/UserListServlet?currentPage=${pagebean.currentPage}">上一页</a>
22 <c:forEach var="bar" items="${requestScope.pagebean.pageBar}">
23 <a href="${pageContext.request.contextPath}/servlet/UserListServlet?currentPage=${pagebean.currentPage}">${bar}</a>
24 </c:forEach>
25 <a href="${pageContext.request.contextPath}/servlet/UserListServlet?currentPage=${pagebean.currentPage}">下一页</a>
26 --%>
27
28 共 ${pagebean.totalRecords} 条记录,
29 每页<input type="text" id="pagesize" value="${pagebean.pageSize }" onchange="gotopage(1)" style=" 30px" maxlength="3">条,
30 共${pagebean.totalPages}页,
31 当前第${pagebean.currentPage}页
32 <a href="javascript:void(0)" onclick="gotopage(${pagebean.previousPage})" >上一页</a>
33 <c:forEach var="bar" items="${requestScope.pagebean.pageBar}">
34 <a href="javascript:void(0)" onclick="gotopage(${bar})" >${bar}</a>
35 </c:forEach>
36 <a href="javascript:void(0)" onclick="gotopage(${pagebean.nextPage})" >下一页</a>
37
38 跳转<input type="text" id="forwardPage" value="${pagebean.currentPage}" style=" 30px;" onchange="gotopage(this.value)">页
39
40 </body>
41
42 <script type="text/javascript">
43 function gotopage(wantedPage) {
44 var pagesize = document.getElementById("pagesize").value;
45 window.location.href = "${pageContext.request.contextPath}/servlet/UserListServlet?currentPage="+wantedPage+"&pageSize="+pagesize;
46
47 }
48
49 </script>
在上面的代码中,我们使用了JSTL标签库的<c:forEach>标签来迭代页面数据内容,也就是PageBean中的contentData集合。中间有一段的代码虽然被注释掉了,这里是用URL地址的方法给每个<a>标签中的href属性赋值超链接,在后面的代码中我使用的是JavaScript的方式。
在Servlet中跳转到JSP页面的请求对象中设置了PageBean对象的关键字:request.setAttribute("pagebean", page); 因此在JSP中,使用EL表达式将以“pagebean”为关键字,而后面跟着PageBean对象的属性取出对应的值。
在JavaScript中,上面无论是改变页面大小、上一页、下一页,某个特定页,跳转某页,都是根据gotopage方法来讲请求超链接发送给Servlet,再一步步发送到数据库查询。在gotopage方法中,传入参数是“wantedPage”,是用户想要去的页数,同时每次该方法调用还会获取页面大小“pagesize”,将这两个数放置在URL地址后作为请求参数给Servlet。
最终效果如下: