在使用Mysql实现分页时,前端一般传递分页参数给后端,后端在把分页列表数据给前端进行展示。这思想没问题。都是这个套路,根据不同的问题,编写不同的代码。
传统分页就是在数据基本不会变化时,就是不会有新数据插入进来,前端一般 是传递 页码,每一页的数量,代码如下
@Data
public class PageEntity implements Serializable {
//页码
public Integer page;
//每一页数量
public Integer limit;
//模糊查询的变量
public String condition;
}
@RequestMapping("/productList")
public ResponseData list(PageEntity pageEntity) {
if (pageEntity == null) {
pageEntity = new PageEntity();
pageEntity.setPage(1);
pageEntity.setLimit(10);
}
int shopId = 1;
PageUtils pageUtils = this.productService.WxListProductPage(shopId, pageEntity);
return ResponseData.success(pageUtils);
}
@Override
public PageUtils WxListProductPage(int shopId, PageEntity pageEntity) {
QueryWrapper<Product> queryWrapper = new QueryWrapper<>();
if (StringUtils.isNotEmpty(pageEntity.condition)) {
queryWrapper.like("product_name", pageEntity.condition);
}
queryWrapper.eq("shop_id", shopId);
queryWrapper.eq("product_state", "ENABLE");
//上面是需要对数据进行筛选
//下面是分页参数的计算
int count = this.baseMapper.selectCount(queryWrapper);
//Mysql的Limit的第一个值
int start = (pageEntity.getPage() - 1) * pageEntity.getLimit();
//Mysql的Limit的第二个值
int num = pageEntity.getLimit();
//调用Mapper查询数据获取返回结果
List<Map<String, Object>> mapList = this.baseMapper.queryWxListProduct(shopId, start, num, pageEntity.getCondition());
return new PageUtils(mapList, count, pageEntity.getLimit(), pageEntity.getPage());
}
<select id="queryWxListProduct" resultType="map">
SELECT
a.id AS id,
c.title AS categoryName,
a.product_name AS productName,
a.num AS num,
a.price AS price,
a.product_desc AS productDesc,
a.create_time AS createTime,
a.update_time AS updateTime,
a.enable_point AS enablePoint,
a.point AS point,
b.file_name AS fileName
FROM product a
LEFT JOIN sys_file_info b ON a.img = b.file_id
LEFT JOIN product_category c ON a.product_category_id = c.id
WHERE
a.shop_id = #{shopId}
AND a.product_state = 'ENABLE'
<if test="condition != null and condition != ''">
AND product_name like CONCAT('%',#{condition},'%')
</if>
ORDER BY
a.create_time DESC
LIMIT #{start}, #{num}
</select>
上面就是传统分页的实现过程,我这是真实项目里面的一部分代码,所以有很多对与本次演示不相关的代码。
而实际情况是后端数据库里面会对我要查询的这张表会经常有新数据产生,而在产生新数据后新的数据会影响到我们分页的查询结果,在查询结果中会出现重复数据,例如下图
第一页的数据
第二页的数据
上述的内容第一页的最后一条出现在了第二页的第一条,就是因为数据库新插入了一条数据,而这条数据影响了我们后台分页的请求参数的计算。
因此,为了不影响用户体验,减少Bug,提高产品质量,我们需要修改代码,然新产生的数据不会对本阶段的分页数据产生影响,需要获取新产生的数据需要刷新从第一页开始取值就可以。每当我开始请求第二页时就不要出现重复数据了。
动态分页的实现过程如下
/**
* 在分页时过滤新插入的数据
* @param headerId 第一次请求下列表中最大的ID
* @param nextId 每一次请求下列表中最小的ID
* @param limit 每一页需要多少条
* @param page 页码,后端无意义
* @return
*/
@RequestMapping("/productListById")
public ResponseData listById(Integer headerId, Integer nextId, Integer limit, Integer page) {
if (limit == null) {
limit = 10;
}
if (page == null) {
page = 1;
}
int shopId = 1;
PageUtils pageUtils = this.productService.WxListProductPageById(shopId, headerId, nextId, limit, page);
return ResponseData.success(pageUtils);
}
@Override
public PageUtils WxListProductPageById(Integer shopId, Integer headerId, Integer nextId,Integer limit,Integer page) {
//一、筛选ID小于等于第一次查询的最大ID的数量
//二、筛选指定店铺的数据
//三、筛选不被禁用的商品
QueryWrapper<Product> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("shop_id", shopId);
queryWrapper.eq("product_state", "ENABLE");
if (headerId != null && headerId > 0) {
queryWrapper.le("id", headerId);
}
int count = this.baseMapper.selectCount(queryWrapper);
List<Map<String, Object>> mapList = this.baseMapper.queryWxListProductById(shopId, 0, limit, nextId);
return new PageUtils(mapList, count, limit, page);
}
<select id="queryWxListProductById" resultType="map">
SELECT
a.id AS id,
c.title AS categoryName,
a.product_name AS productName,
a.num AS num,
a.price AS price,
a.product_desc AS productDesc,
a.create_time AS createTime,
a.update_time AS updateTime,
a.enable_point AS enablePoint,
a.point AS point,
b.file_name AS fileName
FROM
product a
LEFT JOIN sys_file_info b ON a.img = b.file_id
LEFT JOIN product_category c ON a.product_category_id = c.id
WHERE
a.shop_id = #{shopId}
AND a.product_state = 'ENABLE'
<if test="nextId != null and nextId != ''">
AND a.id < #{nextId}
</if>
ORDER BY a.id DESC
LIMIT #{start}, #{num}
</select>
在上面动态分页代码实现中,最重要的参数就是nextId,我们通过nextId去判断下一页的Id应该从哪里开始,我们既然通过了nextId来筛选数据,那么在LIMIT使用就要发生变化,LIMIT的第一个值就一直是0,表示从筛选的数据开始第一个开始取分页数据,因为我们通过nextId过滤了数据,所以不需要第一个值发生改变了,LIMIT的第二个值是需要取的条目数量,在前面我们还有一个headerId,这个参数是第一次请求的时候,获取列表中最大的Id,每一次请求的时候就可以通过这个Id取小于这个Id的条目数量,保证每一次返回的总数量,页码不会发生改变。
我这里使用的是自增长ID,因此我通过ID可以实现,也可以设计字段插入时间,通过插入时间排序后,比较时间段来筛选数据也可以。
最后,贴一下PageUtils类的代码
/**
* 分页工具类
*
*/
public class PageUtils implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 总记录数
*/
private int totalCount;
/**
* 每页记录数
*/
private int pageSize;
/**
* 总页数
*/
private int totalPage;
/**
* 当前页数
*/
private int currPage;
/**
* 列表数据
*/
private List<?> list;
/**
* 分页
*
* @param list 列表数据
* @param totalCount 总记录数
* @param pageSize 每页记录数
* @param currPage 当前页数
*/
public PageUtils(List<?> list, int totalCount, int pageSize, int currPage) {
this.list = list;
this.totalCount = totalCount;
this.pageSize = pageSize;
this.currPage = currPage;
this.totalPage = (int) Math.ceil((double) totalCount / pageSize);
}
/**
* 分页
*/
public PageUtils(IPage<?> page) {
this.list = page.getRecords();
this.totalCount = (int) page.getTotal();
this.pageSize = (int) page.getSize();
this.currPage = (int) page.getCurrent();
this.totalPage = (int) page.getPages();
}
public int getTotalCount() {
return totalCount;
}
public void setTotalCount(int totalCount) {
this.totalCount = totalCount;
}
public int getPageSize() {
return pageSize;
}
public void setPageSize(int pageSize) {
this.pageSize = pageSize;
}
public int getTotalPage() {
return totalPage;
}
public void setTotalPage(int totalPage) {
this.totalPage = totalPage;
}
public int getCurrPage() {
return currPage;
}
public void setCurrPage(int currPage) {
this.currPage = currPage;
}
public List<?> getList() {
return list;
}
public void setList(List<?> list) {
this.list = list;
}
}