概要
分页功能是比较常见的基础功能,虽然比较简单,但是每次需要用到这个功能的时候还是需要现写一遍。为了实现更加宏观的业务复用,特将本人特别喜欢的简易分页逻辑在此记述,以备日后重用。
逻辑描述
一般的分页实现方式多是通过SQL语句“LIMIT”子句进行分页的,如果不清楚LIMIT子句的同学,还请先行了解此子句。
实际上,分页功能最重要的两个参数就是pageSize(每页条数)和pageWant(请求页码),这两个参数都是int型。
pageSize自不必多说,我来说说pageWant,我们常用的“上一页”“下一页”、以及“跳到...页”(当然,对于上一页和下一页的情况,页面需要维护一个全局的当前页的变量,每次请求后都需要更新这个当前页的变量,那么再次发起请求的时候,上一页就是当前页减一,下一页就是当前页加一)都是通过这个参数来请求后端的。这基本解决了前端数据请求的绝大多数情况。另外前端可能需要的几个重要的数值,比如:总页数,总条数 都可以由后台根据相关参数计算生成。
首先,我们可以先定义一个返回值的封装类,这个类中包含了页面分页请求后需要得到的全部数据。
然后,在controller接口的参数列表中,设置int pageSize 和 int pageWant,这里注意,pageSize如果页面可选,则传入,如果就固定条数,甚至可以在后台直接写死即可。
紧接着,将两个分页参数和其他筛选条件一同传入DAO层,由SQL语句直接进行操作和计算。
最后将返回值封装为我们一开始定义的封装类中,直接返回到页面即可。
功能实现
定义返回Wrapper类型
我们的返回值类型中包含页面所需的全部信息,包括基本的总页数,总条数,请求的页码,每页条数,以及最重要的:(经过筛选条件过滤之后的)单页记录列表。如下所示:
import java.util.List;public class PageForDataList<T> {/** 每页条数*/private int pageSize;/** 数据总条数*/private int dataAmount;/** 总页数*/private int pageAmount;/** 请求页数*/private int wantPage;/** 单页记录列表list*/private List<T> dataList;public PageForDataList() {}public PageForDataList(int pageSize, int dataAmount, int pageAmount, int wantPage, List<T> dataList) {super();this.pageSize = pageSize;this.dataAmount = dataAmount;this.pageAmount = pageAmount;this.wantPage = wantPage;this.dataList = dataList;}public int getPageSize() {return pageSize;}public void setPageSize(int pageSize) {this.pageSize = pageSize;}public int getDataAmount() {return dataAmount;}public void setDataAmount(int dataAmount) {this.dataAmount = dataAmount;}public int getPageAmount() {return pageAmount;}public void setPageAmount(int pageAmount) {this.pageAmount = pageAmount;}public int getWantPage() {return wantPage;}public void setWantPage(int wantPage) {this.wantPage = wantPage;}public List<T> getDataList() {return dataList;}public void setDataList(List<T> dataList) {this.dataList = dataList;}
}
设置接口参数
根据页面中的筛选条件的不同,参数有多有少有不同的情况,但是基本都是如下这样的结构:
@ApiOperation("获取特价推荐门票列表,返回值为json结构体")@GetMapping(value = "/special_price/ticket/list")public PageForDataList<SpecialTicketPrice> specialPriceList(@ApiParam(value = "每页条数") @RequestParam(defaultValue = "10", required = true) int pageSize,@ApiParam(value = "请求页数") @RequestParam(defaultValue = "1", required = true) int wantPage,@ApiParam(value = "景区名称") String scenicName, @ApiParam(value = "所在地") String scenicLocation,@ApiParam(value = "推荐状态") Integer rmdStatus, @ApiParam(value = "折扣起始") Double rateStart,@ApiParam(value = "折扣终止") Double rateEnd) {return sprSvc.ticketList(pageSize, wantPage, scenicName, scenicLocation, rmdStatus, rateStart, rateEnd);}
其中,参数列表前两项为分页请求的数据,其余全部是筛选条件。sprSvc是一个service。
DAO层的分页实现
由于我们是通过LIMIT子句来实现分页功能,因此不论如何,都是要将请求的页码传入SQL来操作的。实际上,Service层在一个简单的分页查询的功能中仅仅充当一个Controller层与DAO层数据交互的传递信息的角色。如下service仅供参考:
@Overridepublic PageForDataList<SpecialTicketPrice> ticketList(int pageSize, int wantPage, String scenicName,String scenicLocation, Integer rmdStatus, Double rateStart, Double rateEnd) {// 直接调用DAO中的查询SQLPageForDataList<SpecialTicketPrice> pdl = mngDao.findSpecialTicketList(pageSize, wantPage, scenicName,scenicLocation, rmdStatus, rateStart, rateEnd);return pdl;}
紧接着DAO层的关键实现代码如下:
/*** 分页查询特价推荐门票列表* <br>作者: mht<br> * 时间:2018年5月7日-上午11:07:51<br>* @return*/public PageForDataList<SpecialTicketPrice> findSpecialTicketList(int pageSize, int wantPage, String scenicName, String scenicLocation, Integer rmdStatus, Double rateStart,Double rateEnd) {StringBuilder sqlBuilder = new StringBuilder("SELECT a.* FROM special_ticket_price a, scenic_sequence b WHERE a.seco_scenic_id = b.seco_scenic_id ");if (scenicName != null && !scenicName.equals("")) {sqlBuilder.append("AND b.scenic_name LIKE '%" + scenicName + "%' ");}if (scenicLocation != null) {sqlBuilder.append("AND a.location = '" + scenicLocation + "' ");}if (rmdStatus != null) {sqlBuilder.append("AND a.rmd_status = " + rmdStatus + " ");}if (rateStart != null) {sqlBuilder.append("AND a.discount_rate >= " + rateStart + " ");}if (rateEnd != null) {sqlBuilder.append("AND a.discount_rate <= " + rateEnd + " ");}// 查询总条数sqlString countSql = sqlBuilder.toString().replaceAll("a.\\*", "COUNT(*)");sqlBuilder.append("ORDER BY a.seco_product_id LIMIT " + pageSize * (wantPage - 1) + "," + pageSize);// 查询列表List<SpecialTicketPrice> list = jdbc.query(sqlBuilder.toString(),new BeanPropertyRowMapper<>(SpecialTicketPrice.class));// 查询dataAmount,数据总条数int dataAmount = jdbc.queryForObject(countSql, int.class);return new PageForDataList<SpecialTicketPrice>(pageSize, dataAmount,(int) Math.ceil(1.0 * dataAmount / pageSize), wantPage, list);}
从如上代码中,我们看到,我建立了一个StringBuilder来处理单线程下的查询列表的SQL语句sqlBuilder,然后我利用联表查询,并将五个参数通过if条件拼接到sqlBuilder后。
这里注意,因为不论是什么系统,分页查询一定都是带着筛选条件之后的分页数据列表,这个很好理解,比如我们在某宝买衣服,我以“西服”+ "上衣"作为筛选条件,结果分页之后却出现了裤子、皮鞋、衬衫、内衣等等,这就完全不符合实际需求。换句话说,分页功能的实现一定是建立在筛选条件之下的一个功能。
在代码中,查询总条数的SQL语句的位置很讲究:
// 查询总条数sql
String countSql = sqlBuilder.toString().replaceAll("a.\\*", "COUNT(*)");
可以看到,这句SQL是将SELECT子句中的“a.*”替换为了“COUNT(*)”用于查询符合条件的记录总条数。而其他条件不变。“\\”则是为了完成“*”的转义。
为什么说这句SQL的位置讲究?
因为它是在筛选条件拼接到sqlBuilder之后才进行总数SQL的变化,这恰恰说明了我刚才提到的,分页查询在筛选条件之后的思想。其次,也是非常重要的一点是:countSql的定义,一定要在LIMIT子句之前。换句话说,总数查询的SQL语句一定不能带LIMIT子句!稍微一思考就会明白,我们查询的COUNT(*)应该是符合条件的全部记录条数,也就是在上述代码偏后的位置定义的dataAmount变量,如果COUNT查询在LIMIT子句之后拼入SQL语句(也就是最终得到的是一个带着LIMIT子句的COUNT查询),那么我们查询的结果,也就是记录总条数dataAmount将始终会小于等于pageSize。不服的同学,可以亲自试一试。
( 还要为基础欠佳的同学补充一点的是,请求分页的LIMIT表达式应该符合如下公式:
LIMIT pageSize * (wantPage - 1) , pageSize
其中,pageSize是从前台传入的 1 ,2,3....这样的正整数。)
然后,我们通过jdbcTemplate来对列表查询和COUNT查询的两条SQL语句进行分别查询,并赋值给list和dataAmount,从而得到单页的数据列表和符合条件的总条数。
最后,return的时候,我直接通过最开始定义的返回值封装类的构造器将我们得到的数据进行封装返回到controller层。
其中需要通过数学函数 Math.ceil() 求得的总页数是这样的:
(int) Math.ceil(1.0 * dataAmount / pageSize)
这句话的意思是,用总条数dataAmount除以每页条数pageSize,然后由于除不尽的原因,我们需要事先将dataAmount变换成浮点型数据,然后这样我们就可以得到一个double类型的数据,再通过 Math.ceil() 函数,将浮点型数向上取整,再强转int得到结果。比如,dataAmount = 19,pageSize = 10,那么如上表达式的结果应该是2,也就是总共两页。
最后,我们拿到了这样一个封装好的数据传给controller,再通过return,返回给页面即可。
最终效果展示
由于是测试数据,因此数据并不多,我们可以通过执行的SQL在数据库中做同样的查询看一下结果:
综上,就是对分页功能的简单实现。
如有疑问,欢迎文末留言。