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

