SpringBoot封裝實現(xiàn)分頁查詢工具
概述
在Spring Boot項目中使用MyBatis進行分頁查詢,通常有兩種主流方式:
- 使用MyBatis內(nèi)置的RowBounds進行內(nèi)存分頁(不推薦,數(shù)據(jù)量大時性能差)
- 使用分頁插件,如PageHelper
使用PageHelper可能遇到的一些問題
PageHelper 是一個非常流行的 MyBatis 分頁插件,但它也有一些潛在的缺點和需要注意的地方。以下是在使用 PageHelper 時可能遇到的一些問題:
1.線程安全問題
問題描述:PageHelper.startPage() 方法使用了 ThreadLocal 來保存分頁參數(shù)。如果在同一個線程中多次調(diào)用 startPage() 而沒有及時清理(比如在 finally 塊中調(diào)用 PageHelper.clearPage()),或者線程被復(fù)用(如線程池場景),可能導(dǎo)致分頁參數(shù)混亂。
2.對復(fù)雜 SQL 的支持有限
問題描述:PageHelper 通過攔截 SQL 并重寫來實現(xiàn)分頁。對于特別復(fù)雜的 SQL(例如包含多個嵌套子查詢、UNION 等),自動生成的 count 查詢語句可能會出錯,導(dǎo)致分頁結(jié)果不正確。
3.性能問題
Count 查詢效率:默認情況下,PageHelper 會執(zhí)行一個 count 查詢獲取總記錄數(shù)。如果表數(shù)據(jù)量非常大(上千萬),這個 count 操作可能很慢(尤其是沒有合適索引時)。
4.與其它攔截器沖突
問題描述:如果項目中同時使用了多個 MyBatis 攔截器(如數(shù)據(jù)權(quán)限攔截器、加解密攔截器等),攔截器的執(zhí)行順序可能影響 PageHelper 的正常工作(因為分頁依賴于改寫 SQL)。確保 PageHelper 是最后一個執(zhí)行的攔截器(可以通過調(diào)整攔截器添加順序?qū)崿F(xiàn))。
5.對異步/多線程支持不友好
問題描述:由于依賴 ThreadLocal,如果在異步任務(wù)(如 @Async)或多線程環(huán)境中使用 PageHelper,分頁參數(shù)可能無法正確傳遞到子線程。
6.返回對象過于臃腫
問題描述:PageInfo 對象包含大量分頁信息(如總頁數(shù)、導(dǎo)航頁碼列表等),但實際業(yè)務(wù)中可能只需要部分字段(如當前頁數(shù)據(jù)、總記錄數(shù))。
7.設(shè)計耦合
問題描述:分頁邏輯侵入業(yè)務(wù)代碼(Service 層中顯式調(diào)用 PageHelper.startPage()),違反了分層設(shè)計的純粹性。
建議:PageHelper 適合中小型項目的快速開發(fā),但在高并發(fā)、大數(shù)據(jù)量、復(fù)雜SQL場景下需謹慎使用,必要時采用更可控的分頁方案。
自定義分頁查詢工具
我們可以在Spring Boot項目中不使用PageHelper,而是自己封裝一個分頁查詢工具。主要思路如下:
- 定義一個分頁請求參數(shù)類,包含頁碼和每頁數(shù)量。
- 定義一個分頁結(jié)果類,包含數(shù)據(jù)列表、總記錄數(shù)、總頁數(shù)、當前頁碼、每頁數(shù)量等信息。
- 定義一個分頁查詢工具:先查詢總數(shù),再查詢當前頁數(shù)據(jù),然后封裝成分頁結(jié)果對象。
- 使用MyBatis Dynamic SQL自定義復(fù)雜分頁查詢邏輯:一個用于查詢符合條件的總記錄數(shù),一個用于查詢當前頁的數(shù)據(jù)(使用數(shù)據(jù)庫的分頁語法,如MySQL的LIMIT)。
下面我們一步步實現(xiàn):
步驟1:創(chuàng)建分頁請求參數(shù)類(PageRequest)
步驟2:創(chuàng)建分頁結(jié)果類(PageResult)
步驟3:創(chuàng)建分頁查詢工具(PaginationUtils)
步驟4:在Mapper接口中使用MyBatis Dynamic SQL自定義復(fù)雜分頁查詢邏輯
步驟5:在Service層調(diào)用Mapper的兩個方法,并封裝PageResult
步驟6:在Controller中接收分頁參數(shù),調(diào)用Service方法
分頁查詢具體代碼實現(xiàn)
1. 分頁請求參數(shù)類(PageRequest)
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.Pattern;
import java.util.List;
import java.util.Set;
/**
* 分頁請求參數(shù)封裝類
*/
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";
/**
* 最大允許的每頁記錄數(shù)
*/
public static final int MAX_PAGE_SIZE = 1000;
/**
* 當前頁碼(從1開始)
*/
@Min(value = 1, message = "頁碼不能小于1")
private int pageNum = DEFAULT_PAGE_NUM;
/**
* 每頁記錄數(shù)
*/
@Min(value = 1, message = "每頁數(shù)量不能小于1")
@Max(value = MAX_PAGE_SIZE, message = "每頁數(shù)量不能超過" + 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;
// 無參構(gòu)造器
public PageRequest() {
}
/**
* 帶頁碼和每頁數(shù)量的構(gòu)造器
*
* @param pageNum 當前頁碼
* @param pageSize 每頁數(shù)量
*/
public PageRequest(int pageNum, int pageSize) {
this.pageNum = pageNum;
this.pageSize = pageSize;
}
/**
* 帶所有參數(shù)的構(gòu)造器
*
* @param pageNum 當前頁碼
* @param pageSize 每頁數(shù)量
* @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;
}
/**
* 計算偏移量(用于數(shù)據(jù)庫分頁查詢)
*
* @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. 分頁結(jié)果類(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; // 每頁數(shù)量
private final long total; // 總記錄數(shù)
private final int totalPage; // 總頁數(shù)
private final List<T> data; // 當前頁數(shù)據(jù)
private final String sort; // 排序字段
private final String order; // 排序方向
/**
* 構(gòu)造函數(shù)
*
* @param pageRequest 分頁請求
* @param total 總記錄數(shù)
* @param data 當前頁數(shù)據(jù)
*/
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;
}
/**
* 構(gòu)造函數(shù)
*
* @param pageNum 當前頁碼
* @param pageSize 每頁數(shù)量
* @param total 總記錄數(shù)
* @param data 當前頁數(shù)據(jù)
* @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;
// 計算總頁數(shù)
this.totalPage = calculateTotalPage(total, pageSize);
}
/**
* 計算總頁數(shù)
*
* @param total 總記錄數(shù)
* @param size 每頁數(shù)量
* @return 總頁數(shù)
*/
private int calculateTotalPage(long total, int size) {
if (size <= 0) return 0;
return (int) Math.ceil((double) total / size);
}
// ================ 實用靜態(tài)方法 ================ //
/**
* 創(chuàng)建空的分頁結(jié)果
*
* @param <T> 數(shù)據(jù)類型
* @return 空的分頁結(jié)果
*/
public static <T> PageResult<T> empty() {
return new PageResult<>(1, 0, 0, Collections.emptyList(), null, null);
}
/**
* 基于 PageRequest 創(chuàng)建空的分頁結(jié)果
*
* @param pageRequest 分頁請求
* @param <T> 數(shù)據(jù)類型
* @return 空的分頁結(jié)果
*/
public static <T> PageResult<T> empty(PageRequest pageRequest) {
return new PageResult<>(
pageRequest.getPageNum(),
pageRequest.getPageSize(),
0,
Collections.emptyList(),
pageRequest.getSort(),
pageRequest.getOrder()
);
}
/**
* 創(chuàng)建單頁結(jié)果(適用于數(shù)據(jù)量小的情況)
*
* @param data 所有數(shù)據(jù)
* @param <T> 數(shù)據(jù)類型
* @return 單頁結(jié)果
*/
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);
}
/**
* 創(chuàng)建分頁結(jié)果(基于 PageRequest)
*
* @param pageRequest 分頁請求
* @param total 總記錄數(shù)
* @param data 當前頁數(shù)據(jù)
* @param <T> 數(shù)據(jù)類型
* @return 分頁結(jié)果
*/
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()
);
}
/**
* 轉(zhuǎn)換分頁結(jié)果的數(shù)據(jù)類型
*
* @param source 源分頁結(jié)果
* @param mapper 數(shù)據(jù)轉(zhuǎn)換函數(shù)
* @param <T> 源數(shù)據(jù)類型
* @param <R> 目標數(shù)據(jù)類型
* @return 轉(zhuǎn)換后的分頁結(jié)果
*/
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()
);
}
/**
* 合并兩個分頁結(jié)果(適用于并行查詢場景)
*
* @param result1 第一個分頁結(jié)果
* @param result2 第二個分頁結(jié)果
* @param combiner 數(shù)據(jù)合并函數(shù)
* @param <T> 第一個結(jié)果的數(shù)據(jù)類型
* @param <U> 第二個結(jié)果的數(shù)據(jù)類型
* @param <R> 合并后的數(shù)據(jù)類型
* @return 合并后的分頁結(jié)果
*/
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");
}
// 驗證數(shù)據(jù)數(shù)量是否一致
if (result1.getData().size() != result2.getData().size()) {
throw new IllegalArgumentException("Data lists have different sizes");
}
// 合并數(shù)據(jù)
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. 創(chuàng)建分頁查詢工具(PaginationUtils)
import java.util.List;
import java.util.function.Supplier;
public class PaginationUtils {
/**
* 執(zhí)行分頁查詢(使用PageRequest對象)
*
* @param pageRequest 分頁請求(包含頁碼、大小、排序等信息)
* @param countFunction 查詢總數(shù)的函數(shù)
* @param dataFunction 查詢數(shù)據(jù)的函數(shù)
* @return 分頁結(jié)果
*/
public static <T> PageResult<T> paginate(PageRequest pageRequest,
Supplier<Long> countFunction,
Supplier<List<T>> dataFunction) {
// 查詢總數(shù)
long total = countFunction.get();
// 如果沒有數(shù)據(jù),直接返回空結(jié)果
if (total == 0) {
return PageResult.empty(pageRequest);
}
// 查詢當前頁數(shù)據(jù)
List<T> data = dataFunction.get();
return new PageResult<>(pageRequest, total, data);
}
}4. Mapper接口示例(使用MyBatis Dynamic SQL)
當進行JOIN或復(fù)雜子查詢時,查詢結(jié)果通常涉及多個實體,因此需要自定義結(jié)果映射。MyBatis Dynamic SQL本身不處理結(jié)果映射,你需要:
- 使用注解:在Mapper接口的方法上使用
@Results和@Result注解定義映射關(guān)系。 - 使用XML:在Mapper XML文件中定義
<resultMap>。
例如,對于規(guī)則和規(guī)則版本(一對多)的JOIN查詢,結(jié)果封裝到一個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接口結(jié)果映射配置如下:
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處理復(fù)雜JOIN和子查詢
告別繁瑣的 XML 和 OGNL:
- 痛點: 傳統(tǒng)的 MyBatis XML Mapper 文件雖然功能強大,但編寫和閱讀動態(tài) SQL(使用
<if>,<choose>,<when>,<otherwise>,<foreach>等標簽)在復(fù)雜場景下會變得冗長、嵌套深、可讀性下降,且需要掌握 OGNL 表達式。在 Java 和 XML 之間切換也影響開發(fā)效率。 - 解決: Dynamic SQL 將 SQL 構(gòu)建邏輯完全移回 Java 代碼中,利用 Java 語言的流程控制 (if/else, 循環(huán)) 和強大的 IDE 支持(代碼補全、重構(gòu)、導(dǎo)航),開發(fā)體驗更流暢、更現(xiàn)代。
比如有如下的一個sql語句,獲取滿足條件的規(guī)則及其最新版本信息:
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 實現(xiàn)如下【處理復(fù)雜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.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;
// 數(shù)據(jù)查詢
public SelectStatementProvider buildDataQuery(RuleQueryCondition queryCondition, PageRequest pageRequest) {
// 1. 創(chuàng)建派生表的別名和列定義
// 子查詢的表別名
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);
// 動態(tài)構(gòu)建排序
List<SortSpecification> sortSpecs = new ArrayList<>();
SortSpecification sortSpecification = buildSortSpecification(pageRequest.getSort(), pageRequest.getOrder());
if (sortSpecification != null) {
sortSpecs.add(sortSpecification);
}
// 2.構(gòu)建子查詢
QueryExpressionDSL<SelectModel>.GroupByFinisher maxVersionSubQuery = buildMaxVersionSubQuery(queryCondition);
// 3. 主查詢:關(guān)聯(lián)規(guī)則表、版本表和最大版本子查詢
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);
}
// 總數(shù)查詢
public SelectStatementProvider buildCountQuery(RuleQueryCondition queryCondition) {
// 1. 創(chuàng)建派生表的別名和列定義
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. 構(gòu)建子查詢
QueryExpressionDSL<SelectModel>.GroupByFinisher maxVersionSubQuery = buildMaxVersionSubQuery(queryCondition);
// 3. 主查詢:關(guān)聯(lián)規(guī)則表、版本表和最大版本子查詢
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);
}
// 公共方法:構(gòu)建最大版本子查詢
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;
}
}傳統(tǒng) mapper.xml(XML 動態(tài) 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);
}關(guān)鍵差異對比
| 特性 | MyBatis Dynamic SQL | 傳統(tǒng) mapper.xml |
|---|---|---|
| 代碼類型 | Java 代碼 | XML 配置文件 |
| 可讀性 | ???? (強類型檢查) | ?? (需切換文件查看) |
| 編譯時檢查 | ? 類型安全 | ? 運行時發(fā)現(xiàn)錯誤 |
| 動態(tài)條件 | 鏈式方法調(diào)用 (如 .where(...)) | <if>/<choose> 標簽 |
| 子查詢支持 | 通過 DSL 嵌套構(gòu)建 | 原生 SQL 寫法 |
| 分頁控制 | .limit()/.offset() 方法 | LIMIT 直接拼接 |
| 維護成本 | 中 (需學(xué)習(xí) DSL 語法) | 低 (SQL 原生寫法) |
| 適合場景 | 復(fù)雜動態(tài)查詢、高復(fù)用邏輯 | 簡單查詢、團隊熟悉 XML 語法 |
推薦選擇:
- 新項目推薦 MyBatis Dynamic SQL:類型安全 + 更好的重構(gòu)能力
- 遺留系統(tǒng)或簡單查詢可用 mapper.xml:降低學(xué)習(xí)成本
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, "排序字段不合法");
// 構(gòu)建查詢語句
SelectStatementProvider selectStatementProvider = ruleQueryBuilder.buildDataQuery(condition, pageRequest);
System.out.println(selectStatementProvider.getSelectStatement());
// 構(gòu)建總數(shù)查詢語句
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);
}
}總結(jié)
自己封裝分頁查詢雖然代碼量稍多,但可控性強,避免了PageHelper的線程安全問題,適合對分頁有定制需求或高并發(fā)場景。
以上就是SpringBoot封裝實現(xiàn)分頁查詢工具的詳細內(nèi)容,更多關(guān)于SpringBoot分頁查詢的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Linux配置jdk1.8與jdk17兼容并存并啟動jar包指定jdk版本
JDK是Java語言的軟件開發(fā)工具包,主要用于移動設(shè)備、嵌入式設(shè)備上的java應(yīng)用程序,這篇文章主要給大家介紹了關(guān)于Linux配置jdk1.8與jdk17兼容并存并啟動jar包指定jdk版本的相關(guān)資料,需要的朋友可以參考下2024-08-08
SpringBoot2.2.X用Freemarker出現(xiàn)404的解決
這篇文章主要介紹了SpringBoot2.2.X用Freemarker出現(xiàn)404的解決,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-02-02
ConcurrentModificationException日志關(guān)鍵字報警思考分析
本文將記錄和分析日志中的ConcurrentModificationException關(guān)鍵字報警,還有一些我的思考,有需要的朋友可以借鑒參考下,希望能夠有所幫助2023-12-12
SpringBoot如何將applicaton.properties中的數(shù)據(jù)綁定到bean中
SpringBoot中我們該如何將applicaton.properties中的數(shù)據(jù)綁定到bean中呢?在之間我們是可以通過@value注解,在SpringBoot自動啟動后,會自動的去相應(yīng)的路徑中去尋找applicaton.properties配置文件,然后將相應(yīng)的值賦值上,感興趣的朋友一起看看吧2025-05-05
java并發(fā)包中CountDownLatch和線程池的使用詳解
這篇文章主要介紹了java并發(fā)包中CountDownLatch和線程池的使用詳解,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2021-02-02

