Spring Boot项目自己封装一个分页查询工具

概述

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

  1. 使用MyBatis内置的RowBounds进行内存分页(不推荐,数据量大时性能差)

  2. 使用分页插件,如PageHelper

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

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

  1. 线程安全问题
  • 问题描述PageHelper.startPage() 方法使用了 ThreadLocal 来保存分页参数。如果在同一个线程中多次调用 startPage() 而没有及时清理(比如在 finally 块中调用 PageHelper.clearPage()),或者线程被复用(如线程池场景),可能导致分页参数混乱。
  1. 对复杂 SQL 的支持有限
  • 问题描述:PageHelper 通过拦截 SQL 并重写来实现分页。对于特别复杂的 SQL(例如包含多个嵌套子查询、UNION 等),自动生成的 count 查询语句可能会出错,导致分页结果不正确。
  1. 性能问题
  • Count 查询效率:默认情况下,PageHelper 会执行一个 count 查询获取总记录数。如果表数据量非常大(上千万),这个 count 操作可能很慢(尤其是没有合适索引时)。
  1. 与其它拦截器冲突
  • 问题描述:如果项目中同时使用了多个 MyBatis 拦截器(如数据权限拦截器、加解密拦截器等),拦截器的执行顺序可能影响 PageHelper 的正常工作(因为分页依赖于改写 SQL)。确保 PageHelper 是最后一个执行的拦截器(可以通过调整拦截器添加顺序实现)。
  1. 对异步/多线程支持不友好
  • 问题描述 :由于依赖 ThreadLocal,如果在异步任务(如 @Async)或多线程环境中使用 PageHelper,分页参数可能无法正确传递到子线程。
  1. 返回对象过于臃肿
  • 问题描述PageInfo 对象包含大量分页信息(如总页数、导航页码列表等),但实际业务中可能只需要部分字段(如当前页数据、总记录数)。
  1. 设计耦合
  • 问题描述 :分页逻辑侵入业务代码(Service 层中显式调用 PageHelper.startPage()),违反了分层设计的纯粹性。

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

自定义分页查询工具

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

  1. 定义一个分页请求参数类,包含页码和每页数量。

  2. 定义一个分页结果类,包含数据列表、总记录数、总页数、当前页码、每页数量等信息。

  3. 定义一个分页查询工具:先查询总数,再查询当前页数据,然后封装成分页结果对象。

  4. 使用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):

java 复制代码
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):

java 复制代码
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 pageRequest 分页请求
     * @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)

java 复制代码
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)中:

java 复制代码
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接口结果映射配置如下:

java 复制代码
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);
}

4. 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语句,获取满足条件的规则及其最新版本信息:

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_version 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和子查询】:

java 复制代码
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.RenderingStrategies;
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)

xml 复制代码
<!-- 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:降低学习成本

5. Service层:

java 复制代码
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));

    }

}
  1. Controller层:
java 复制代码
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的线程安全问题,适合对分页有定制需求或高并发场景。

相关推荐
Victor35642 分钟前
Redis(25)Redis的RDB持久化的优点和缺点是什么?
后端
Victor35643 分钟前
Redis(24)如何配置Redis的持久化?
后端
ningqw8 小时前
SpringBoot 常用跨域处理方案
java·后端·springboot
你的人类朋友8 小时前
vi编辑器命令常用操作整理(持续更新)
后端
胡gh8 小时前
简单又复杂,难道只能说一个有箭头一个没箭头?这种问题该怎么回答?
javascript·后端·面试
一只叫煤球的猫9 小时前
看到同事设计的表结构我人麻了!聊聊怎么更好去设计数据库表
后端·mysql·面试
uzong9 小时前
技术人如何对客做好沟通(上篇)
后端
颜如玉10 小时前
Redis scan高位进位加法机制浅析
redis·后端·开源
Moment10 小时前
毕业一年了,分享一下我的四个开源项目!😊😊😊
前端·后端·开源
why技术11 小时前
在我眼里,这就是天才般的算法!
后端·面试