# scaffold 项目之数据分页

# 简介

为什么数据要分页呢?

  1. 性能优化
    • 减少内存消耗:如果一次性加载大量数据,可能会导致内存溢出或性能下降。
    • 减少数据库负载:分页可以减少数据库单次查询返回的数据量,降低数据库的负载和查询时间。
  2. 用户体验
    • 快速响应:用户不需要等待所有数据加载完成,可以更快地查看页面内容。
    • 易于浏览:分页允许用户通过页码快速跳转到数据的不同部分,而不是滚动一个很长的页面。
  3. 网络带宽节省
    • 减少数据传输:分页可以减少每次请求传输的数据量,节省网络带宽,特别是在移动网络或低速网络环境下。
  4. 数据管理
    • 易于维护:分页可以帮助开发者更好地管理和维护数据,尤其是在处理大量数据时。
  5. 安全性
    • 防止数据泄露:限制单次请求返回的数据量可以减少敏感信息泄露的风险。
  6. 可扩展性
    • 适应数据增长:随着数据量的增加,分页可以确保系统的性能不会受到太大影响。
  7. 符合用户习惯
    • 用户习惯分页浏览:大多数用户习惯于在电商平台、社交媒体等网站上使用分页浏览数据。
  8. API 设计
    • 符合 RESTful 原则:在设计 RESTful API 时,分页可以帮助客户端更有效地获取资源。
  9. 避免全表扫描
    • 提高查询效率:分页通常涉及到数据库的索引查询,相比于全表扫描,可以更快地定位到需要的数据。
  10. 业务需求
    • 满足业务场景:某些业务场景下,如报表、数据分析等,需要对大量数据进行分页展示。

可以使用 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 接口中,有几个显著的好处:

  1. 代码复用
    • default 方法允许你在接口中直接提供实现,这意味着你可以在接口级别共享通用的逻辑,而不需要在每个实现类中重复相同的代码。
  2. 减少模板代码
    • 通过在接口中定义 default 方法,你可以减少实现类中的模板代码,使得实现类更加简洁,专注于特定的业务逻辑。
  3. 提高可维护性
    • 当需要修改通用逻辑时,你只需在接口中修改 default 方法,而不需要修改每个实现类,这降低了维护成本。
  4. 增强可读性
    • 将通用逻辑放在接口中,可以让其他开发者更容易理解接口的预期行为,提高了代码的可读性。
  5. 便于单元测试
    • 由于 default 方法提供了默认实现,你可以更容易地为接口编写单元测试,因为你可以模拟或直接使用这些默认方法。
  6. 实现延迟
    • default 方法允许你在不立即提供具体实现的情况下定义接口,实现可以延迟到需要的时候再提供,这在设计初期特别有用。
  7. 兼容性
    • 如果你有一个现有的接口,需要添加新的方法而不影响现有实现, default 方法是一个不错的选择,因为它允许旧的实现继续工作而不需要修改。
  8. 提供扩展点
    • default 方法提供了一个扩展点,允许实现类根据需要重写这些方法,以提供定制的行为。

在你提供的 DictDataMapper 接口中, default 方法 selectPage 提供了一个分页查询的默认实现,这个实现使用了 MyBatis Plus 的 LambdaQueryWrapperX 来构建查询条件,并执行分页查询。这样做的好处是:

  • 简化分页查询:客户端代码可以直接调用这个默认方法来执行分页查询,而不需要关心分页查询的具体实现细节。
  • 提高代码复用性:如果多个地方需要执行类似的分页查询,可以直接使用这个默认方法,而不需要重复编写相同的代码。
  • 保持接口的简洁性:接口的实现类不需要重复实现分页查询逻辑,只需要关注于特定的业务逻辑。

总的来说, default 方法提供了一种在接口中实现共享逻辑的有效方式,有助于提高代码的可维护性、可读性和可测试性。