# scaffold 项目之数据分页
# 简介
为什么数据要分页呢?
- 性能优化:
- 减少内存消耗:如果一次性加载大量数据,可能会导致内存溢出或性能下降。
- 减少数据库负载:分页可以减少数据库单次查询返回的数据量,降低数据库的负载和查询时间。
- 用户体验:
- 快速响应:用户不需要等待所有数据加载完成,可以更快地查看页面内容。
- 易于浏览:分页允许用户通过页码快速跳转到数据的不同部分,而不是滚动一个很长的页面。
- 网络带宽节省:
- 减少数据传输:分页可以减少每次请求传输的数据量,节省网络带宽,特别是在移动网络或低速网络环境下。
- 数据管理:
- 易于维护:分页可以帮助开发者更好地管理和维护数据,尤其是在处理大量数据时。
- 安全性:
- 防止数据泄露:限制单次请求返回的数据量可以减少敏感信息泄露的风险。
- 可扩展性:
- 适应数据增长:随着数据量的增加,分页可以确保系统的性能不会受到太大影响。
- 符合用户习惯:
- 用户习惯分页浏览:大多数用户习惯于在电商平台、社交媒体等网站上使用分页浏览数据。
- API 设计:
- 符合 RESTful 原则:在设计 RESTful API 时,分页可以帮助客户端更有效地获取资源。
- 避免全表扫描:
- 提高查询效率:分页通常涉及到数据库的索引查询,相比于全表扫描,可以更快地定位到需要的数据。
- 业务需求:
- 满足业务场景:某些业务场景下,如报表、数据分析等,需要对大量数据进行分页展示。
可以使用 Spring Data JPA、MyBatis 分页插件或者自定义 SQL 来实现数据分页。本项目用的是 MyBatis Plus。
# 分页的实现
- 前端:基于 Element UI 分页组件 Pagination
- 后端:基于 MyBatis Plus 分页功能,二次封装
# 前端分页实现
略
# 后端分页实现
# controller
package com.tz.scaffold.module.system.controller.admin.dict; | |
/** | |
* <p> Project: scaffold - DictDataController </p> | |
* | |
* 管理后台 - 字典数据 | |
* @author Tz | |
* @date 2024/01/09 23:45 | |
* @version 1.0.0 | |
* @since 1.0.0 | |
*/ | |
@Tag(name = "管理后台 - 字典数据") | |
@RestController | |
@RequestMapping("/system/dict-data") | |
@Validated | |
public class DictDataController { | |
@Resource | |
private DictDataService dictDataService; | |
@GetMapping("/page") | |
@Operation(summary = "/获得字典类型的分页列表") | |
@PreAuthorize("@ss.hasPermission('system:dict:query')") | |
public CommonResult<PageResult<DictDataRespVO>> getDictTypePage(@Valid DictDataPageReqVO reqVO) { | |
return success(DictDataConvert.INSTANCE.convertPage(dictDataService.getDictDataPage(reqVO))); | |
} | |
} |
Request 分页请求,使用
DictDataPageReqVO
类,它继承 PageParam 类Response 分页结果,使用
PageResult
类,每一项是DictDataRespVO
类
# 分页参数 PageParam
分页请求,需要继承 PageParam
类。代码如下:
package com.tz.scaffold.framework.common.pojo; | |
import io.swagger.v3.oas.annotations.media.Schema; | |
import lombok.Data; | |
import javax.validation.constraints.Min; | |
import javax.validation.constraints.Max; | |
import javax.validation.constraints.NotNull; | |
import java.io.Serializable; | |
/** | |
* <p> Project: scaffold - PageParam </p> | |
* | |
* 分页参数 | |
* @author Tz | |
* @date 2024/01/09 23:45 | |
* @version 1.0.0 | |
* @since 1.0.0 | |
*/ | |
@Schema(description="分页参数") | |
@Data | |
public class PageParam implements Serializable { | |
private static final Integer PAGE_NO = 1; | |
private static final Integer PAGE_SIZE = 10; | |
/** | |
* 每页条数 - 不分页 | |
* | |
* 例如说,导出接口,可以设置 {@link #pageSize} 为 -1 不分页,查询所有数据。 | |
*/ | |
public static final Integer PAGE_SIZE_NONE = -1; | |
@Schema(description = "页码,从 1 开始", requiredMode = Schema.RequiredMode.REQUIRED,example = "1") | |
@NotNull(message = "页码不能为空") | |
@Min(value = 1, message = "页码最小值为 1") | |
private Integer pageNo = PAGE_NO; | |
@Schema(description = "每页条数,最大值为 100", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") | |
@NotNull(message = "每页条数不能为空") | |
@Min(value = 1, message = "每页条数最小值为 1") | |
@Max(value = 100, message = "每页条数最大值为 100") | |
private Integer pageSize = PAGE_SIZE; | |
} |
分页条件,在子类中进行定义。以 TenantPageReqVO 举例子,代码如下:
package com.tz.scaffold.module.system.controller.admin.dict.vo.data; | |
import com.tz.scaffold.framework.common.pojo.PageParam; | |
import io.swagger.v3.oas.annotations.media.Schema; | |
import lombok.Data; | |
import lombok.EqualsAndHashCode; | |
import javax.validation.constraints.Size; | |
/** | |
* <p> Project: scaffold - DictDataPageReqVO </p> | |
* | |
* 管理后台 - 字典类型分页列表 Request VO | |
* @author Tz | |
* @date 2024/01/09 23:45 | |
* @version 1.0.0 | |
* @since 1.0.0 | |
*/ | |
@Schema(description = "管理后台 - 字典类型分页列表 Request VO") | |
@Data | |
@EqualsAndHashCode(callSuper = true) | |
public class DictDataPageReqVO extends PageParam { | |
@Schema(description = "字典标签", example = "芋道") | |
@Size(max = 100, message = "字典标签长度不能超过100个字符") | |
private String label; | |
@Schema(description = "字典类型,模糊匹配", example = "sys_common_sex") | |
@Size(max = 100, message = "字典类型类型长度不能超过100个字符") | |
private String dictType; | |
@Schema(description = "展示状态,参见 CommonStatusEnum 枚举类", example = "1") | |
private Integer status; | |
} |
# 分页结果 PageResult
分页结果 PageResult
类,代码如下:
package com.tz.scaffold.framework.common.pojo; | |
import io.swagger.v3.oas.annotations.media.Schema; | |
import lombok.Data; | |
import java.io.Serializable; | |
import java.util.ArrayList; | |
import java.util.List; | |
/** | |
* <p> Project: scaffold - PageResult </p> | |
* | |
* 分页结果 | |
* @param <T> 数据泛型 | |
* @author Tz | |
* @date 2024/01/09 23:45 | |
* @version 1.0.0 | |
* @since 1.0.0 | |
*/ | |
@Schema(description = "分页结果") | |
@Data | |
public final class PageResult<T> implements Serializable { | |
@Schema(description = "数据", requiredMode = Schema.RequiredMode.REQUIRED) | |
private List<T> list; | |
@Schema(description = "总量", requiredMode = Schema.RequiredMode.REQUIRED) | |
private Long total; | |
public PageResult() { | |
} | |
public PageResult(List<T> list, Long total) { | |
this.list = list; | |
this.total = total; | |
} | |
public PageResult(Long total) { | |
this.list = new ArrayList<>(); | |
this.total = total; | |
} | |
public static <T> PageResult<T> empty() { | |
return new PageResult<>(0L); | |
} | |
public static <T> PageResult<T> empty(Long total) { | |
return new PageResult<>(total); | |
} | |
} |
分页结果的数据 list
的每一项,通过自定义 VO 类,例如说 DictDataRespVO
类。
# Mapper 查询
在 dictDataMapper
类中,定义 selectPage 查询方法。代码如下:
package com.tz.scaffold.module.system.dal.mysql.dict; | |
import com.tz.scaffold.framework.common.pojo.PageResult; | |
import com.tz.scaffold.framework.mybatis.core.mapper.BaseMapperX; | |
import com.tz.scaffold.framework.mybatis.core.query.LambdaQueryWrapperX; | |
import com.tz.scaffold.module.system.controller.admin.dict.vo.data.DictDataExportReqVO; | |
import com.tz.scaffold.module.system.controller.admin.dict.vo.data.DictDataPageReqVO; | |
import com.tz.scaffold.module.system.dal.dataobject.dict.DictDataDO; | |
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; | |
import org.apache.ibatis.annotations.Mapper; | |
import java.util.Arrays; | |
import java.util.Collection; | |
import java.util.List; | |
/** | |
* <p> Project: scaffold - DictDataMapper </p> | |
* | |
* 字典数据 Mapper | |
* @author Tz | |
* @date 2024/01/09 23:45 | |
* @version 1.0.0 | |
* @since 1.0.0 | |
*/ | |
@Mapper | |
public interface DictDataMapper extends BaseMapperX<DictDataDO> { | |
default PageResult<DictDataDO> selectPage(DictDataPageReqVO reqVO) { | |
return selectPage(reqVO, new LambdaQueryWrapperX<DictDataDO>() | |
.likeIfPresent(DictDataDO::getLabel, reqVO.getLabel()) | |
.eqIfPresent(DictDataDO::getDictType, reqVO.getDictType()) | |
.eqIfPresent(DictDataDO::getStatus, reqVO.getStatus()) | |
.orderByDesc(Arrays.asList(DictDataDO::getDictType, DictDataDO::getSort))); | |
} | |
} |
针对 MyBatis Plus 分页查询的二次分装,在 BaseMapperX
中实现,主要是将 MyBatis 的分页结果 IPage,转换成项目的分页结果 PageResult。代码如下图:
package com.tz.scaffold.framework.mybatis.core.mapper; | |
import cn.hutool.core.collection.CollUtil; | |
import com.tz.scaffold.framework.common.pojo.PageParam; | |
import com.tz.scaffold.framework.common.pojo.PageResult; | |
import com.tz.scaffold.framework.mybatis.core.util.MyBatisUtils; | |
import com.baomidou.mybatisplus.core.conditions.Wrapper; | |
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; | |
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; | |
import com.baomidou.mybatisplus.core.mapper.BaseMapper; | |
import com.baomidou.mybatisplus.core.metadata.IPage; | |
import com.baomidou.mybatisplus.core.toolkit.support.SFunction; | |
import com.baomidou.mybatisplus.extension.toolkit.Db; | |
import com.github.yulichang.base.MPJBaseMapper; | |
import com.github.yulichang.interfaces.MPJBaseJoin; | |
import org.apache.ibatis.annotations.Param; | |
import java.util.Collection; | |
import java.util.List; | |
/** | |
* <p> Project: scaffold - BaseMapperX </p> | |
* | |
* 在 MyBatis Plus 的 BaseMapper 的基础上拓展,提供更多的能力 | |
* <p> | |
* <li> | |
* 1. {@link BaseMapper} 为 MyBatis Plus 的基础接口,提供基础的 CRUD 能力 | |
* <li> | |
* 2. {@link MPJBaseMapper} 为 MyBatis Plus Join 的基础接口,提供连表 Join 能力 | |
* @author Tz | |
* @date 2024/01/09 23:45 | |
* @version 1.0.0 | |
* @since 1.0.0 | |
*/ | |
public interface BaseMapperX<T> extends MPJBaseMapper<T> { | |
default PageResult<T> selectPage(PageParam pageParam, @Param("ew") Wrapper<T> queryWrapper) { | |
// MyBatis Plus 查询 将 pageNo + pageSize 拼接成条件 | |
IPage<T> mpPage = MyBatisUtils.buildPage(pageParam); | |
// 执行分页查询 | |
selectPage(mpPage, queryWrapper); | |
// 转换返回 | |
return new PageResult<>(mpPage.getRecords(), mpPage.getTotal()); | |
} | |
} |
为什么接口中使用默认的
default
方法?在 Java 接口中使用默认的
default
方法,特别是在像DictDataMapper
这样的 MyBatis Mapper 接口中,有几个显著的好处:
- 代码复用:
default
方法允许你在接口中直接提供实现,这意味着你可以在接口级别共享通用的逻辑,而不需要在每个实现类中重复相同的代码。- 减少模板代码:
- 通过在接口中定义
default
方法,你可以减少实现类中的模板代码,使得实现类更加简洁,专注于特定的业务逻辑。- 提高可维护性:
- 当需要修改通用逻辑时,你只需在接口中修改
default
方法,而不需要修改每个实现类,这降低了维护成本。- 增强可读性:
- 将通用逻辑放在接口中,可以让其他开发者更容易理解接口的预期行为,提高了代码的可读性。
- 便于单元测试:
- 由于
default
方法提供了默认实现,你可以更容易地为接口编写单元测试,因为你可以模拟或直接使用这些默认方法。- 实现延迟:
default
方法允许你在不立即提供具体实现的情况下定义接口,实现可以延迟到需要的时候再提供,这在设计初期特别有用。- 兼容性:
- 如果你有一个现有的接口,需要添加新的方法而不影响现有实现,
default
方法是一个不错的选择,因为它允许旧的实现继续工作而不需要修改。- 提供扩展点:
default
方法提供了一个扩展点,允许实现类根据需要重写这些方法,以提供定制的行为。在你提供的
DictDataMapper
接口中,default
方法selectPage
提供了一个分页查询的默认实现,这个实现使用了 MyBatis Plus 的LambdaQueryWrapperX
来构建查询条件,并执行分页查询。这样做的好处是:
- 简化分页查询:客户端代码可以直接调用这个默认方法来执行分页查询,而不需要关心分页查询的具体实现细节。
- 提高代码复用性:如果多个地方需要执行类似的分页查询,可以直接使用这个默认方法,而不需要重复编写相同的代码。
- 保持接口的简洁性:接口的实现类不需要重复实现分页查询逻辑,只需要关注于特定的业务逻辑。
总的来说,
default
方法提供了一种在接口中实现共享逻辑的有效方式,有助于提高代码的可维护性、可读性和可测试性。