开发者

SpringBoot封装实现分页查询工具

目录
  • 概述
    • 使用PageHelper可能遇到的一些问题
    • 自定义分页查询工具
  • 分页查询具体代码实现
    • 1. 分页请求参数类(PageRequest)
    • 2. 分页结果类(PageResult)
    • 3. 创建分页查询工具(PaginationUtils)
    • 4. Mapper接口示例(使用MyBATis Dynamic SQL)
    • 5. MyBatis Dynamic SQL处理复杂JOIN和子查询
    • 6. Service层
  • 总结

    概述

    在Spring Boot项目中使用MyBatis进行分页查询,通常有两种主流方式:

    • 使用MyBatis内置的RowBounds进行内存分页(不推荐,数据量大时性能差)
    • 使用分页插件,如PageHelper

    使用PageHelper可能遇到的一些问题

    PageHelper 是一个非常流行的 MyBatis 分页插件,但它也有一些潜在的缺点和需要注意的地方。以下是在使用 PageHelper 时可能遇到的一些问题:

    1.线程安全问题

    问题描述PageHelper.startPage() 方法使用了 ThreadLocal 来保存分页参数。如果在同一个线程中多次调用 startPage() 而没有及时清理(比如在 finally 块中调用 PageHelper.clearPage()),或者线程被复用(如线程池场景),可能导致分页参数混乱。

    2.对复杂 SQL 的支持有限

    问题描述:PageHelper 通过拦截 SQL 并重写来实现分页。对于特别复杂的 SQL(例如包含多个嵌套子查询、UNION 等),自动生成的 count 查询语句可能会出错,导致分页结果不正确。

    3.性能问题

    Count 查询效率:默认情况下,PageHelper 会执行一个 count 查询获取总记录数。如果表数据量非常大(上千万),这个 count 操作可能很慢(尤其是没有合适索引时)。

    4.与其它拦截器冲突

    问题描述:如果项目中同时使用了多个 MyBatis 拦截器(如数据权限拦截器、加解密拦截器等),拦截器的执行顺序可能影响 PageHelper 的正常工作(因为分页依赖于改写 SQL)。确保 PageHelper 是最后一个执行的拦截器(可以通过调整拦截器添加顺序实现)。

    5.对异步/多线程支持不友好

    问题描述:由于依赖 ThreadLocal,如果在异步任务(如 @Async)或多线程环境中使用 PageHelper,分页参数可能无法正确传递到子线程。

    6.返回对象过于臃肿

    问题描述PageInfo 对象包含大量分页信息(如总页数、导航页码列表等),但实际业务中可能只需要部分字段(如当前页数据、总记录数)。

    7.设计耦合

    问题描述:分页逻辑侵入业务代码(Service 层中显式调用 PageHelper.startPage()),违反了http://www.devze.com分层设计的纯粹性。

    建议:PageHelper 适合中小型项目的快速开发,但在高并发、大数据量、复杂SQL场景下需谨慎使用,必要时采用更可控的分页方案。

    自定义分页查询工具

    我们可以在Spring Boot项目中不使用PageHelper,而是自己封装一个分页查询工具。主要思路如下:

    • 定义一个分页请求参数类,包含页码和每页数量。
    • 定义一个分页结果类,包含数据列表、总记录数、总页数、当前页码、每页数量等信息。
    • 定义一个分页查询工具:先查询总数,再查询当前页数据,然后封装成分页结果对象。
    • 使用MyBatis Dynamic SQL自定义复杂分页查询逻辑:一个用于查询符合条件的总记录数,一个用于查询当前页的数据(使用数据库的分页语法,如mysql的LIMIT)。

    下面我们一步步实现:

    步骤1:创建分页请求参数类(PageRequest)

    步骤2:创建分页结果类(PageResult)

    步骤3:创建分页查询工具(PaginationUtils)

    步骤4:在Mapper接口中使用MyBatis Dynamic SQL自定义复杂分页查询逻辑

    步骤5:在Service层调用Mapper的两个方法,并封装PageResult

    步骤6:在Controller中接收分页参数,调用Service方法

    分页查询具体代码实现

    1. 分页请求参数类(PageRequest)

    import jakarta.validation.constraints.Max;
    import jakarta.validation.constraints.Min;
    import jakarta.validation.constraints.Pattern;
    import Java.util.List;
    import java.util.Set;
    /**
     * 分页请求参数封装类
     */
    public class PageRequest {
        /**
         * 默认第一页
         */
        public static final int DEFAULT_PAGE_NUM = 1;
        /**
         * 默认每页10条
         */
        public static final int DEFAULT_PAGE_SIZE = 10;
        /**
         * 默认排序方向 - 升序
         */
        public static final String DEFAULT_ORDER = "desc";
        /**
         * 最大允许的每页记录数
         */
        public static final int MAX_PAGE_SIZE = 1000;
        /**
         * 当前页码(从1开始)
         */
        @Min(value = 1, message = "页码不能小于1")
        private int pageNum = DEFAULT_PAGE_NUM;
        /**
         * 每页记录数
         */
        @Min(value = 1, message = "每页数量不能小于1")
        @Max(value = MAX_PAGE_SIZE, message = "每页数量不能超过" + MAX_PAGE_SIZE)
        private int pageSize = DEFAULT_PAGE_SIZE;
        /**
         * 排序字段
         */
        private String sort;
        /**
         * 排序方向
         * asc: 升序
         * desc: 降序
         */
        @Pattern(regexp = "asc|desc", message = "排序方向必须是asc或desc")
        private String order = DEFAULT_ORDER;
        // 无参构造器
        public PageRequest() {
        }
        /**
         * 带页码和每页数量的构造器
         *
         * @param pageNum  当前页码
         * @param pageSize 每页数量
         */
        public PageRequest(int pageNum, int pageSize) {
            this.pageNum = pageNum;
            this.pageSize = pageSize;
        }
        /**
         * 带所有参数的构造器
         *
         * @param pageNum  当前页码
         * @param pageSize 每页数量
         * @param sort     排序字段
         * @param order    排序方向
         */
        public PageRequest(int pageNum, int pageSize, String sort, String order) {
            this.pageNum = pageNum;
            this.pageSize = pageSize;
            this.sort = sort;
            this.order = order;
        }
        /**
         * 计算偏移量(用于数据库分页查询)
         *
         * @return 当前页的起始位置
         */
        public int getOffset() {
            return (pageNum - 1) * pageSize;
        }
        /**
         * 验证排序字段是否在允许的列表中
         *
         * @param allowedFields 允许的排序字段集合
         * @return 如果排序字段有效返回true,否则返回false
         */
        public boolean isSortValid(Set<String> allowedFields) {
            if (sort == null || sort.isEmpty()) {
                return true;
            }
            return allowedFields.contains(sort);
        }
        /**
         * 验证排序字段是否在允许的列表中,无效时抛出异常
         *
         * @param allowedFields 允许的排序字段集合
         * @param errorMessage  错误信息
         * @throws IllegalArgumentException 如果排序字段无效
         */
        public void validateSort(List<String> allowedFields, String errorMessage) {
            if (sort != null && !sort.isEmpty() && !allowedFields.contains(sort)) {
                throw new IllegalArgumentException(errorMessage);
            }
        }
        public int getPageNum() {
            return pageNum;
        }
        public void setPageNum(int pageNum) {
            this.pageNum = pageNum;
        }
        public int getPageSize() {
            return pageSize;
        }
        public void setPageSize(int pageSize) {
            this.pageSize = pageSize;
        }
        public String getSort() {
            return sort;
        }
        public void setSort(String sort) {
            this.sort = sort;
        }
        public String getOrder() {
            return order;
        }
        public void setOrder(String order) {
            this.order = order;
        }
    }

    2. 分页结果类(PageResult)

    import java.util.ArrayList;
    import java.util.Collections;
    import java.util.List;
    import java.util.function.BiFunction;
    import java.util.function.Function;
    import java.util.stream.Collectors;
    public class PageResult<T> {
        private final int pageNum;        // 当前页码
        private final int pageSize;        // 每页数量
        private final long total;      // 总记录数
        private final int totalPage;       // 总页数
        private final List<T> data;    // 当前页数据
        private final String sort;     // 排序字段
        private final String order;    // 排序方向
        /**
         * 构造函数
         *
         * @param pageRequest 分页请求
         * @param total       总记录数
         * @param data        当前页数据
         */
        public PageResult(PageRequest pageRequest, long total, List<T> data) {
            this.pageNum = pageRequest.getPageNum();
            this.pageSize = pageRequest.getPageSize();
            this.sort = pageRequest.getSort();
            this.order = pageRequest.getOrder();
            this.total = total;
            this.totalPage = calculateTotalPage(total, pageRequest.getPageSize());
            this.data = data;
        }
        /**
         * 构造函数
         *
         * @param pageNum  当前页码
         * @param pageSize  每页数量
         * @param total 总记录数
         * @param data  当前页数据
         * @param sort  排序字段
         * @param order 排序方向
         */
        public PageResult(int pageNum, int pageSize, long total, List<T> data, String sort, String order) {
            this.pageNum = pageNum;
            this.pageSize = pageSize;
            this.total = total;
            this.data = data != null ? data : Collections.emptyList();
            this.sort = sort;
            this.order = order;
            // 计算总页数
            this.totalPage = calculateTotalPage(total, pageSize);
        }
        /**
         * 计算总页数
         *
         * @param total 总记录数
         * @param size  每页数量
         * @return 总页数
         */
        private int calculateTotalPage(long total, int size) {
            if (size <= 0) return 0;
            return (int) Math.ceil((double) total / size);
        }
        // ================ 实用静态方法 ================ //
        /**
         * 创建空的分页结果
         *
         * @param <T> 数据类型
         * @return 空的分页结果
         */
        public static <T> PageResult<T> empty() {
            return new PageResult<>(1, 0, 0, Collections.emptyList(), null, null);
        }
        /**
         * 基于 PageRequest 创建空的分页结果
         *
         * @param pageRequest 分页请求
         * @param <T>         数据类型
         * @return 空的分页结果
         */
        public static <T> PageResult<T> empty(PageRequest pageRequest) {
            return new PageResult<>(
                    pageRequest.getPageNum(),
                    pageRequest.getPageSize(),
                    0,
                    Collections.emptyList(),
                    pageRequest.getSort(),
                    pageRequest.getOrder()
            );
        }
        /**
         * 创建单页结果(适用于数据量小的情况)
         *
         * @param data 所有数据
         * @param <T>  数据类型
         * @return 单页结果
         */
        public static <T> PageResult<T> singlePage(List<T> data) {
            long total = data != null ? data.size() : 0;
            return new PageResult<>(1, (int) total, total, data, null, null);
        }
        /**
         * 创建分页结果(基于 PageRequest)
         *
         * @param pahttp://www.devze.comgeRequest 分页请求
         * @param total       总记录数
         * @param data        当前页数据
         * @param <T>         数据类型
         * @return 分页结果
         */
        public static <T> PageResult<T> of(PageRequest pageRequest, long total, List<T> data) {
            return new PageResult<>(
                    pageRequest.getPageNum(),
                    pageRequest.getPageSize(),
                    total,
                    data,
                    pageRequest.getSort(),
                    pageRequest.getOrder()
            );
        }
        /**
         * 转换分页结果的数据类型
         *
         * @param source 源分页结果
         * @param mapper 数据转换函数
         * @param <T>    源数据类型
         * @param <R>    目标数据类型
         * @return 转换后的分页结果
         */
        public static <T, R> PageResult<R> map(PageResult<T> source, Function<T, R> mapper) {
            if (source == null || mapper == null) {
                throw new IllegalArgumentException("Source and mapper must not be null");
            }
            List<R> mappedData = source.getData().stream()
                    .map(mapper)
                    .collect(Collectors.toList());
            return new PageResult<>(
                    source.getPageNum(),
                    source.getPageSize(),
                    source.getTotal(),
                    mappedData,
                    source.getSort(),
                    source.getOrder()
            );
        }
        /**
         * 合并两个分页结果(适用于并行查询场景)
         *
         * @param result1  第一个分页结果
         * @param result2  第二个分页结果
         * @param combiner 数据合并函数
         * @param <T>      第一个结果的数据类型
         * @param <U>      第二个结果的数据类型
         * @param <R>      合并后的数据类型
         * @return 合并后的分页结果
         */
        public static <T, U, R> PageResult<R> combine(
                PageResult<T> result1,
                PageResult<U> result2,
                BiFunction<T, U, R> combiner) {
            // 验证分页信息是否一致
            if (result1.getPageNum() != result2.getPageNum() ||
                    result1.getPageSize() != result2.getPageSize() ||
                    result1.getTotal() != result2.getTotal()) {
                throw new IllegalArgumentException("Page results are not compatible for combination");
            }
            // 验证数据数量是否一致
            if (result1.getData().size() != result2.getData().size()) {
                throw new IllegalArgumentException("Data lists have different sizes");
            }
            // 合并数据
            List<R> combinedData = new ArrayList<>();
            for (int i = 0; i < result1.getData().size(); i++) {
                R combined = combiner.apply(
                        result1.getData().get(i),
                        result2.getData().get(i)
                );
                combinedData.add(combined);
            }
            return new PageResult<>(
                    result1.getPageNum(),
                    result1.getPageSize(),
                    result1.getTotal(),
                    combinedData,
                    result1.getSort(),
                    result1.getOrder()
            );
        }
        public int getPageNum() {
            return pageNum;
        }
        public int getPageSize() {
            return pageSize;
        }
        public long getTotal() {
            return total;
        }
        public int getTotalPage() {
            return totalPage;
        }
        public List<T> getData() {
            return data;
        }
        public String getSort() {
            return sort;
        }
        public String getOrder() {
            return order;
        }
    }

    3. 创建分页查询工具(PaginationUtils)

    import java.util.List;
    import java.util.function.Supplier;
    public class PaginationUtils {
        /**
         * 执行分页查询(使用PageRequest对象)
         *
         * @param pageRequest   分页请求(包含页码、大小、排序等信息)
         * @param countFunction 查询总数的函数
         * @param dataFunction  查询数据的函数
         * @return 分页结果
         */
        public static <T> PageResult<T> paginate(PageRequest pageRequest,
                                                 Supplier<Long> countFunction,
                                                 Supplier<List<T>> dataFunction) {
            // 查询总数
            long total = countFunction.get();
            // 如果没有数据,直接返回空结果
            if (total == 0) {
                return PageResult.empty(pageRequest);
            }
            // 查询当前页数据
            List<T> data = dataFunction.get();
            return new PageResult<>(pageRequest, total, data);
        }
    }

    4. Mapper接口示例(使用MyBatis Dynamic SQL)

    当进行JOIN或复杂子查询时,查询结果通常涉及多个实体,因此需要自定义结果映射。MyBatis Dynamic SQL本身不处理结果映射,你需要:

    • 使用注解:在Mapper接口的方法上使用@Results@Result注解定义映射关系。
    • 使用XML:在Mapper XML文件中定义<resultMap>

    例如,对于规则和规则版本(一对多)的JOIN查询,结果封装到一个DTO(Data Transfer Object)中:

    import java.util.Date;
    
    public class RuleWithLatestVersionDTO {
        private Long id;
        private String ruleId;
        private String name;
        private String domain;
        private Integer latestVersion;
        private String versionName;
        private String versionStatus;
        private Date versionModifiedDate;
    
        // getters and setters
    
    }

    在 Mapper接口结果映射配置如下:

    import com.example.demo.model.dto.response.RuleWithLatestVersionDTO;
    import org.apache.ibatis.annotations.Mapper;
    import org.apache.ibatis.annotations.Result;
    import org.apache.ibatis.annotations.Results;
    import org.apache.ibatis.annotations.SelectProvider;
    import org.mybatis.dynamic.sql.select.render.SelectStatementProvider;
    import org.mybatis.dynamic.sql.util.SqlProviderAdapter;
    import org.mybatis.dynamic.sql.util.mybatis3.CommonCountMapper;
    import java.util.List;
    @Mapper
    public interface RuleCustomMapper extends CommonCountMapper {
        // 使用@Result注解处理多表字段
        @SelectProvider(type = SqlProviderAdapter.class, method = "select")
        @Results({
                @Result(column = "id", property = "id"),
                @Result(column = "ruleId", property = "ruleId"),
                @Result(column = "name", property = "name"),
                @Result(column = "domain", property = "domain"),
                @Result(column = "latestVersion", property = "latestVersion"),
                @Result(column = "versionName", property = "versionName"),
                @Result(column = "versionStatus", property = "versionStatus"),
                @Result(column = "versionModifiedDate", property = "versionModifiedDate"),
        })
        List<RuleWithLatestVersionDTO> findByCondition(SelectStatementProvider selectStatement);
    }

    5. 编程客栈MyBatis Dynamic SQL处理复杂JOIN和子查询

    告别繁琐的 XML 和 OGNL:

    • 痛点: 传统的 MyBatis XML Mapper 文件虽然功能强大,但编写和阅读动态 SQL(使用 <if>, <choose>, <when>, <otherwise>, <foreach> 等标签)在复杂场景下会变得冗长、嵌套深、可读性下降,且需要掌握 OGNL 表达式。在 Java 和 XML 之间切换也影响开发效率。
    • 解决: Dynamic SQL 将 SQL 构建逻辑完全移回 Java 代码中,利用 Java 语言的流程控制 (if/else, 循环) 和强大的 IDE 支持(代码补全、重构、导航),开发体验更流畅、更现代。

    比如有如下的一个sql语句,获取满足条件的规则及其最新版本信息:

    SELECT ruleTable.id AS id, ruleTable.rule_id AS ruleId, ruleTable.name AS name, ruleTable.domain AS domain, max_version AS latestVersion
        , ruleVersionTable.name AS versionName, ruleVersionTable.status AS versionStatus, ruleVersionTable.gmt_modified AS versionModifiedDate
    FROM rule ruleTable
        JOIN rule_versiosFeWoQn ruleVersionTable ON ruleTable.rule_id = ruleVersionTable.rule_id
        JOIN (
            SELECT ruleVersionTable.rule_id AS rule_uuid, MAX(ruleVersionTable.version) AS max_version
            FROM rule_version ruleVersionTable
            WHERE ruleVersionTable.id > #{parameters.p1,jdbcType=BIGINT}
            GROUP BY ruleVersionTable.rule_id
        ) max_ver
        ON ruleVersionTable.rule_id = max_ver.rule_uuid
            AND ruleVersionTable.version = max_ver.max_version
    WHERE ruleTable.id > #{parameters.p2,jdbcType=BIGINT}
        AND ruleTable.name LIKE #{parameters.p3,jdbcType=VARCHAR}
    ORDER BY ruleVersionTable.id
    LIMIT #{parameters.p5}, #{parameters.p4}
    

    使用 MyBatis Dynamic SQL 实现如下【处理复杂JOIN和子查询】:

    import com.example.demo.common.model.page.PageRequest;
    import com.example.demo.model.query.RuleQueryCondition;
    import com.example.demo.repository.generated.RuleEntityDynamicSqlSupport;
    import com.example.demo.repository.generated.RuleVersionEntityDynamicSqlSupport;
    import org.mybatis.dynamic.sql.SortSpecification;
    import org.mybatis.dynamic.sql.SqlColumn;
    import org.mybatis.dynamic.sql.SqlTable;
    import org.mybatis.dynamic.sql.select.ColumnSortSpecification;
    import org.mybatis.dynamic.sql.select.QueryExpressionDSL;
    import org.mybatis.dynamic.sql.select.SelectModel;
    import org.mybatis.dynamic.sql.select.render.SelectStatementProvider;
    import org.mybatis.dynamic.sql.render.RenderingStrajstegies;
    import org.springframework.stereotype.Component;
    import java.util.ArrayList;
    import java.util.List;
    import java.sql.JDBCType;
    import static org.mybatis.dynamic.sql.SqlBuilder.*;
    @Component
    public class RuleQueryBuilder {
        private final RuleVersionEntityDynamicSqlSupport.RuleVersionEntity ruleVersionDO = RuleVersionEntityDynamicSqlSupport.ruleVersionEntity;
        private final RuleEntityDynamicSqlSupport.RuleEntity ruleDO = RuleEntityDynamicSqlSupport.ruleEntity;
        // 数据查询
        public SelectStatementProvider buildDataQuery(RuleQueryCondition queryCondition, PageRequest pageRequest) {
            // 1. 创建派生表的别名和列定义
            // 子查询的表别名
            String subQueryTable = "max_ver";
            SqlTable maxVerTable = SqlTable.of(subQueryTable);
            SqlColumn<String> maxVerRuleUuid = SqlColumn.of("rule_uuid", maxVerTable, JDBCType.VARCHAR);
            SqlColumn<Integer> maxVerMaxVersion = SqlColumn.of("max_version", maxVerTable, JDBCType.INTEGER);
            // 动态构建排序
            List<SortSpecification> sortSpecs = new ArrayList<>();
            SortSpecification sortSpecification = buildSortSpecification(pageRequest.getSort(), pageRequest.getOrder());
            if (sortSpecification != null) {
                sortSpecs.add(sortSpecification);
            }
            // 2.构建子查询
            QueryExpressionDSL<SelectModel>.GroupByFinisher maxVersionSubQuery = buildMaxVersionSubQuery(queryCondition);
            // 3. 主查询:关联规则表、版本表和最大版本子查询
            return select(
                    ruleDO.id.as("id"),
                    ruleDO.ruleId.as("ruleId"),
                    ruleDO.name.as("name"),
                    ruleDO.domain.as("domain"),
                    maxVerMaxVersion.as("latestVersion"),
                    ruleVersionDO.name.as("versionName"),
                    ruleVersionDO.status.as("versionStatus"),
                    ruleVersionDO.gmtModified.as("versionModifiedDate")
            )
                    .from(ruleDO, "ruleDO")
                    .join(ruleVersionDO, "ruleVersionDO")
                    .on(ruleDO.ruleId, equalTo(ruleVersionDO.ruleId))
                    .join(maxVersionSubQuery, subQueryTable)
                    .on(ruleVersionDO.ruleId, equalTo(maxVerRuleUuid.qualifiedwith(subQueryTable)))
                    .and(ruleVersionDO.version, equalTo(maxVerMaxVersion.qualifiedWith(subQueryTable)))
                    .where(ruleDO.id, isGreaterThan(0L))
                    .and(ruleDO.tenantId, isEqualToWhenPresent(queryCondition.getTenantId()))
                    .and(ruleDO.ruleId, isLikeWhenPresent(wrapLike(queryCondition.getRuleId())))
                    .and(ruleDO.name, isLikeWhenPresent(wrapLike(queryCondition.getName())))
                    .and(ruleDO.creator, isLikeWhenPresent(wrapLike(queryCondition.getCreateBy())))
                    .and(ruleDO.type, isEqualToWhenPresent(queryCondition.getType()))
                    .and(ruleDO.domain, isEqualToWhenPresent(queryCondition.getDomain()))
                    .and(ruleDO.description, isLikeWhenPresent(wrapLike(queryCondition.getDescription())))
                    .orderBy(sortSpecs.toArray(new SortSpecification[0]))
                    .limit(pageRequest.getPageSize())
                    .offset(pageRequest.getOffset())
                    .build()
                    .render(RenderingStrategies.MYBATIS3);
        }
        // 总数查询
        public SelectStatementProvider buildCountQuery(RuleQueryCondition queryCondition) {
            // 1. 创建派生表的别名和列定义
            String subQueryTable = "max_ver";
            SqlTable maxVerTable = SqlTable.of(subQueryTable);
            SqlColumn<String> maxVerRuleUuid = SqlColumn.of("rule_uuid", maxVerTable, JDBCType.VARCHAR);
            SqlColumn<Integer> maxVerMaxVersion = SqlColumn.of("max_version", maxVerTable, JDBCType.INTEGER);
            // 2. 构建子查询
            QueryExpressionDSL<SelectModel>.GroupByFinisher maxVersionSubQuery = buildMaxVersionSubQuery(queryCondition);
            // 3. 主查询:关联规则表、版本表和最大版本子查询
            return select(count())
                    .from(ruleDO, "ruleDO")
                    .join(ruleVersionDO, "ruleVersionDO")
                    .on(ruleDO.ruleId, equalTo(ruleVersionDO.ruleId))
                    .join(maxVersionSubQuery, subQueryTable)
                    .on(ruleVersionDO.ruleId, equalTo(maxVerRuleUuid.qualifiedWith(subQueryTable)))
                    .and(ruleVersionDO.version, equalTo(maxVerMaxVersion.qualifiedWith(subQueryTable)))
                    .where(ruleVersionDO.id, isGreaterThan(0L))  // 确保where条件有值
                    .and(ruleDO.tenantId, isEqualToWhenPresent(queryCondition.getTenantId()))
                    .and(ruleDO.ruleId, isLikeWhenPresent(wrapLike(queryCondition.getRuleId())))
                    .and(ruleDO.name, isLikeWhenPresent(wrapLike(queryCondition.getName())))
                    .and(ruleDO.creator, isLikeWhenPresent(wrapLike(queryCondition.getCreateBy())))
                    .and(ruleDO.type, isEqualToWhenPresent(queryCondition.getType()))
                    .and(ruleDO.domain, isEqualToWhenPresent(queryCondition.getDomain()))
                    .and(ruleDO.description, isLikeWhenPresent(wrapLike(queryCondition.getDescription())))
                    .build()
                    .render(RenderingStrategies.MYBATIS3);
        }
        // 公共方法:构建最大版本子查询
        private QueryExpressionDSL<SelectModel>.GroupByFinisher buildMaxVersionSubQuery(RuleQueryCondition queryCondition) {
            return select(
                    ruleVersionDO.ruleId.as("rule_uuid"),
                    max(ruleVersionDO.version).as("max_version"))
                    .from(ruleVersionDO)
                    .where(ruleVersionDO.id, isGreaterThan(0L))
                    .and(ruleVersionDO.modifier, isLikeWhenPresent(wrapLike(queryCondition.getUpdateBy())))
                    .and(ruleVersionDO.gmtCreate, isGreaterThanOrEqualToWhenPresent(queryCondition.getGmtCreateFrom()))
                    .and(ruleVersionDO.gmtCreate, isLessThanOrEqualToWhenPresent(queryCondition.getGmtCreateTo()))
                    .and(ruleVersionDO.gmtModified, isGreaterThanOrEqualToWhenPresent(queryCondition.getGmtModifiedFrom()))
                    .and(ruleVersionDO.gmtModified, isLessThanOrEqualToWhenPresent(queryCondition.getGmtModifiedTo()))
                    .and(ruleVersionDO.description, isLikeWhenPresent(wrapLike(queryCondition.getRuleVersionDesc())))
                    .and(ruleVersionDO.name, isLikeWhenPresent(wrapLike(queryCondition.getRuleVersionName())))
                    .and(ruleVersionDO.status, isEqualToWhenPresent(queryCondition.getStatus()))
                    .groupBy(ruleVersionDO.ruleId);
        }
        private SortSpecification buildSortSpecification(String field, String order) {
            if (field == null) {
                return new ColumnSortSpecification("ruleVersionDO", ruleVersionDO.id);
            }
            ColumnSortSpecification columnSortSpecification;
            switch (field) {
                case "gmtCreate" ->
                        columnSortSpecification = new ColumnSortSpecification("ruleVersionDO", ruleVersionDO.gmtCreate);
                case "gmtModified" ->
                        columnSortSpecification = new ColumnSortSpecification("ruleVersionDO", ruleVersionDO.gmtModified);
                // 其他字段...
                // 默认排序逻辑
                default -> columnSortSpecification = new ColumnSortSpecification("ruleVersionDO", ruleVersionDO.id);
            }
            return "asc".equalsIgnoreCase(order) ? columnSortSpecification : columnSortSpecification.descending();
        }
        private String wrapLike(String value) {
            return value != null ? "%" + value + "%" : null;
        }
    }

    传统 mapper.xml(XML 动态 SQL)

    <!-- 1. 定义查询语句 -->
    <select id="selectRulesWithLatestVersion" resultType="RuleWithLatestVersionDTO">
      SELECT 
        ruleTable.id AS id,
        ruleTable.rule_id AS ruleId,
        ruleTable.name AS name,
        ruleTable.domain AS domain,
        max_ver.max_version AS latestVersion,
        ruleVersionTable.name AS versionName,
        ruleVersionTable.status AS versionStatus,
        ruleVersionTable.gmt_modified AS versionModifiedDate
      FROM rule ruleTable
      JOIN rule_version ruleVersionTable 
        ON ruleTable.rule_id = ruleVersionTable.rule_id
      JOIN (
        SELECT 
          rule_id AS rule_uuid, 
          MAX(version) AS max_version
        FROM rule_version
        <where>
          <if test="p1 != null">
            AND id > #{p1}
          </if>
        </where>
        GROUP BY rule_id
      ) max_ver 
        ON ruleVersionTable.rule_id = max_ver.rule_uuid
        AND ruleVersionTable.version = max_ver.max_version
      <where>
        <if test="p2 != null">
          AND ruleTable.id > #{p2}
        </if>
        <if test="p3 != null">
          AND ruleTable.name LIKE CONCAT('%', #{p3}, '%')
        </if>
      </where>
      ORDER BY ruleVersionTable.id
      LIMIT #{p5}, #{p4}
    </select>
    
    <!-- 2. Mapper 接口 -->
    public interface RuleMapper {
        List<RuleWithLatestVersionDTO> selectRulesWithLatestVersion(
            @Param("p1") Long p1, 
            @Param("p2") Long p2,
            @Param("p3") String namePattern,
            @Param("p4") Integer pageSize,
            @Param("p5") Integer offset);
    }

    关键差异对比

    特性MyBatis Dynamic SQL传统 mapper.xml
    代码类型Java 代码XML 配置文件
    可读性⭐⭐⭐⭐ (强类型检查)⭐⭐ (需切换文件查看)
    编译时检查✅ 类型安全❌ 运行时发现错误
    动态条件链式方法调用 (如 .where(...))<if>/<choose> 标签
    子查询支持通过 DSL 嵌套构建原生 SQL 写法
    分页控制.limit()/.offset() 方法LIMIT 直接拼接
    维护成本中 (需学习 DSL 语法)低 (SQL 原生写法)
    适合场景复杂动态查询、高复用逻辑简单查询、团队熟悉 XML 语法

    推荐选择

    • 新项目推荐 MyBatis Dynamic SQL:类型安全 + 更好的重构能力
    • 遗留系统或简单查询可用 mapper.xml:降低学习成本

    6. Service层

    import com.example.demo.common.model.page.PageRequest;
    import com.example.demo.common.model.page.PageResult;
    import com.example.demo.common.model.page.PaginationUtils;
    import com.example.demo.model.dto.response.RuleWithLatestVersionDTO;
    import com.example.demo.model.query.RuleQueryCondition;
    import com.example.demo.repository.custom.RuleCustomMapper;
    import com.example.demo.repository.custom.builder.RuleQueryBuilder;
    import com.example.demo.repository.generated.RuleMapper;
    import com.example.demo.service.RuleService;
    import org.mybatis.dynamic.sql.SqlColumn;
    import org.mybatis.dynamic.sql.select.render.SelectStatementProvider;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    import java.util.Arrays;
    import java.util.List;
    @Service
    public class RuleServiceImpl implements RuleService {
        private final RuleCustomMapper ruleCustomMapper;
        private final RuleMapper ruleMapper;
        private final RuleQueryBuilder ruleQueryBuilder;
        @Autowired
        public RuleServiceImpl(RuleMapper ruleMapper, RuleCustomMapper ruleCustomMapper, RuleQueryBuilder ruleQueryBuilder) {
            this.ruleMapper = ruleMapper;
            this.ruleCustomMapper = ruleCustomMapper;
            this.ruleQueryBuilder = ruleQueryBuilder;
        }
        @Override
        public PageResult<RuleWithLatestVersionDTO> findByCondition(RuleQueryCondition condition, PageRequest pageRequest) {
            List<String> columNames = Arrays.stream(ruleMapper.selectList).map(c -> ((SqlColumn<?>) c).name()).toList();
            pageRequest.validateSort(columNames, "排序字段不合法");
            // 构建查询语句
            SelectStatementProvider selectStatementProvider = ruleQueryBuilder.buildDataQuery(condition, pageRequest);
            System.out.println(selectStatementProvider.getSelectStatement());
            // 构建总数查询语句
            SelectStatementProvider countQuery = ruleQueryBuilder.buildCountQuery(condition);
            System.out.println(countQuery.getSelectStatement());
            return PaginationUtils.paginate(pageRequest,
                    () -> ruleMapper.count(countQuery),
                    () -> ruleCustomMapper.findByCondition(selectStatementProvider));
        }
    }

    Controller层:

    package com.example.demo.controller;
    import com.example.demo.common.model.page.PageResult;
    import com.example.demo.common.model.response.Result;
    import com.example.demo.model.dto.request.RuleQueryPageRequest;
    import com.example.demo.model.dto.response.RuleWithLatestVersionDTO;
    import com.example.demo.service.RuleService;
    import jakarta.validation.Valid;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.validation.annotation.Validated;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestBody;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    @RestController
    @RequestMapping("/api")
    @Validated
    public class RuleController {
        private final RuleService ruleService;
        @Autowired
        public RuleController(RuleService ruleService) {
            this.ruleService = ruleService;
        }
        @PostMapping("/rule")
        public Result<PageResult<RuleWithLatestVersionDTO>> queryRule(@Valid @RequestBody RuleQueryPageRequest request) {
            PageResult<RuleWithLatestVersionDTO> pageResult = ruleService.findByCondition(request.getQueryCondition(), request.getPageRequest());
            return Result.success(pageResult);
        }
    }

    总结

    自己封装分页查询虽然代码量稍多,但可控性强,避免了PageHelper的线程安全问题,适合对分页有定制需求或高并发场景。

    以上就是SpringBoot封装实现分页查询工具的详细内容,更多关于SpringBoot分页查询的资料请关注编程客栈(www.devze.com)其它相关文章!

    0

    上一篇:

    下一篇:

    精彩评论

    暂无评论...
    验证码 换一张
    取 消

    最新开发

    开发排行榜