本篇讲诉如何在页面中通过操作数据库来完成数据显示的分页功能。当一个操作数据库进行查询的语句返回的结果集内容如果过多,那么内存极有可能溢出,所以在大数据的情况下分页是必须的。当然分页能通过很多种方式来实现,而这里我们采用的是操作数据库的方式,而且在这种方式中,最重要的是带限制条件的查询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="width: 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="width: 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。
最终效果如下: