前言
??在開發 Web 應用時,我們經常需要處理海量數據的展示問題。例如,在一個電商平臺上,商品列表可能有成千上萬條數據。如果我們一次性將所有數據返回給前端,不僅會導致頁面加載緩慢,還會對數據庫造成巨大壓力。為了解決這個問題,分頁查詢應運而生。所謂分頁查詢,就是當查詢結果數據較多時,采用按頁顯示的方法將數據分成若干個“頁面”,每次只加載當前頁面的數據,而不是一次性全部顯示。通過分頁可以有效減少單次查詢的返回數據量,提升性能和用戶體驗。PageHelper 是國內非常優秀的一款開源的 MyBatis 分頁插件,能夠幫助我們快速實現分頁功能。而且它支持基本主流與常用的數據庫, 例如mysql、 oracle、mariaDB、 DB2、 SQLite、Hsqldb等,今天就給大家聊聊 PageHelper 這款分頁插件。
一、PageHelper 概述
1.1 什么是 PageHelper
??在開發過程中實現分頁查詢,我們可以使用 SQL 語句中添加 limit 關鍵字的方法實現分頁查詢。但是查詢分頁內容時,需要計算相關的分頁信息和參數。而且無論什么業務其分頁邏輯都是類似的,所以有框架幫助我們高效實現分頁功能。PageHelper 是一個基于 MyBatis 的分頁插件,它通過攔截 MyBatis 的執行器,在 SQL 語句執行前后動態添加分頁邏輯來實現分頁功能。通過簡單的配置和調用,PageHelper 可以自動處理 SQL 查詢中的分頁邏輯,極大地簡化了分頁功能的開發過程,支持多種分頁方式和結果集排序、篩選等操作。
- PageHelper 官網:https://pagehelper.github.io
- PageHelper 源碼:https://gitee.com/free/Mybatis_PageHelper/
- PageHelper API:https://apidoc.gitee.com/free/Mybatis_PageHelper/
1.2 PageHelper 的工作原理
??PageHelper 的工作原理主要依賴于攔截 MyBatis 的查詢操作,在查詢前設置分頁參數,并在執行 SQL 語句時動態添加分頁邏輯,從而實現分頁查詢。它通過修改當前執行的 SQL 語句來添加分頁條件,執行添加了分頁條件的 SQL 語句,最終返回分頁后的結果集。此外,PageHelper 還提供了詳細的配置選項和默認參數支持,如pagehelper.reasonable、pagehelper.defaultCount等,用戶可以根據自己的需求進行配置。在整合PageHelper到項目中時,需要確保已經正確導入了MyBatis的依賴,并且按照官方文檔的指引進行依賴的引入和配置。
??總的來說,PageHelper 是一款功能強大且易于使用的 MyBatis 分頁插件,它大大簡化了分頁查詢的實現過程,提高了開發效率,是 Java 項目中實現分頁功能的常用工具之一。
二、PageHelper 的基本使用
2.1 引入依賴
??在使用 PageHelper 之前,需要在項目中引入 PageHelper 的相關依賴。如果使用的是 Maven,可以在 pom.xml 中添加以下依賴:
<dependency><groupId>com.github.pagehelper</groupId><artifactId>pagehelper</artifactId><version>x.y.z</version>
</dependency>
??若是 Spring Boot中集成 PageHelper,需要在 pom.xml 中添加以下依賴即可,無需額外配置,PageHelper 會自動集成 MyBatis。
<dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>x.y.z</version>
</dependency><dependency><groupId>com.github.pagehelper</groupId><artifactId>pagehelper-spring-boot-starter</artifactId><version>x.y.z</version>
</dependency>
2.2 配置攔截器插件
在 MyBatis 配置文件中配置
??在 MyBatis 的配置文件中(通常是 mybatis-config.xml ),添加 PageHelper 的插件配置,關鍵代碼如下所示:
<plugins><plugin interceptor="com.github.pagehelper.PageInterceptor"> <!-- 配置全局的分頁參數 --><property name="helperDialect" value="mysql"/><property name="offsetAsPageNum" value="true"/><property name="rowBoundsWithCount" value="true"/></plugin>
</plugins>
在 Spring Boot中配置
?Spring Boot 引入 starter 后自動生效,對分頁插件進行配置時,通常可以通過配置文件 application.properties
或 application.yaml
中進行配置,關鍵代碼如下所示:
pagehelper:helper-dialect: mysql # 配置分頁插件的方言,即使用的什么數據庫則就用什么數據庫reasonable: true # 開啟合理查詢:即若超過最大頁跳到最后一頁,若查詢-1頁,默認查詢第一頁。support-methods-arguments: true # 通過 Mapper 接口方法的參數來傳遞分頁參數params: count=countSql # 指定count查詢的參數名稱。
??或者可以通過配置類定義設置相關的參數信息,關鍵代碼如下所示:
import com.github.pagehelper.PageHelper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Properties;@Configuration
public class MyBatisConfig {@Beanpublic PageHelper pageHelper() {PageHelper pageHelper = new PageHelper();Properties properties = new Properties();properties.setProperty("dialect", "Mysql");properties.setProperty("offsetAsPageNum", "true");properties.setProperty("rowBoundsWithCount", "true");pageHelper.setProperties(properties);return pageHelper;}
}
2.3 編寫 Mapper 接口和 XML 文件
為了實現 PageHelper,定義一個實體類,關鍵代碼如下所示:
@Data
public class SysUser {private Long id;private String userName;private String password;private String phone;private String realName;private String nickName;private String email;// 賬戶狀態(1.正常 2.鎖定 )private Integer status;// 性別(1.男 2.女)private Integer sex;// 是否刪除(1未刪除;0已刪除)private Integer deleted;private Long createUserId;private Long updateUserId;private Date createTime;private Date updateTime;
}
??編寫相關 Mapper 接口和對應的 XML 文件,這些文件應該包含需要進行分頁查詢的 SQL 語句。注意,這里不需要特別修改 SQL 語句以支持分頁,因為 PageHelper 會自動處理。假設我們有一個 UserMapper 接口,用于查詢用戶數據,關鍵代碼如下所示:
@Mapper
public interface SysUserMapper {List<SysUser> selectUserList(Page<SysUser> page);
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace="com.dllwh.mybatis.mapper.SysUserMapper"><select id="selectUserList" resultType="com.dllwh.mybatis.model.SysUser">SELECT * FROM sys_user</select>
</mapper>
2.4 實現分頁查詢
??在 Service 層或 Controller 層中,我們可以通過 PageHelper 提供的方法來進行分頁參數的設置和獲取分頁結果。PageHelper 的核心方法是 PageHelper.startPage()
,它的作用是為當前線程開啟分頁上下文,并在接下來的查詢中攔截 SQL,添加分頁參數。關鍵代碼如下所示:
public interface UserService {PageInfo<User> selectUserList(int pageNum, int pageSize);
}@Service
public class UserServiceImpl implements UserService{@Resource private SysUserMapper sysUserMapper;public PageInfo<User> selectUserList(int pageNum, int pageSize) {// 這句代碼要放在查詢 mapper 語句的前面,用于對數據庫查詢 SQL 設置分頁參數PageHelper.startPage(pageNum, pageSize);// 執行查詢操作List<User> userList = sysUserMapper.selectUserList(null);// 將查詢到的結果對象封裝到pageInfo中,可以查看分頁的各種數據return new PageInfo<>(userList);}
}
??PageHelper 利用 MyBatis 的插件機制攔截查詢語句,在查詢 SQL 中自動加入分頁語法,如 MySQL 的 LIMIT
或 Oracle 的 ROWNUM
,并執行兩次 SQL 查詢:
- 查詢總記錄數:執行
SELECT COUNT(*) FROM ...
獲取滿足條件的記錄總數。 - 查詢分頁數據:在原始查詢 SQL 后追加分頁條件。
2.5 返回分頁結果
??在 Controller 層中,我們可以將分頁結果返回給前端,關鍵代碼如下所示:
@RestController
@RequestMapping("/user")
public class UserController {@Resource private UserService userService;@GetMapping("/list")public PageInfo<User> getUsersList(int pageNum, int pageSize) {return userService.getUsersList(pageNum, pageSize);}
}
??從服務層獲取到分頁數據后,就可以在前端頁面上進行展示,并添加分頁導航條等控件來控制分頁。
三、PageHelper 的核心功能
分頁對象 Page
??Page 是一個接口,它包含分頁數據以及一些基本的分頁信息(如總記錄數、當前頁等)。當使用 PageHelper 進行分頁查詢時,查詢結果會被自動封裝到一個實現了 Page 接口的對象中。
public class Page<E> extends ArrayList<E> implements Closeable {// 頁碼,從1開始private int pageNum;// 頁面大小private int pageSize;// 起始行private long startRow;// 末行private long endRow;// 總數private long total;// 總頁數private int pages;// 包含count查詢private boolean count = true;// 分頁合理化private Boolean reasonable;// 當設置為true的時候,如果pagesize設置為0(或RowBounds的limit=0),就不執行分頁,返回全部結果private Boolean pageSizeZero;// 進行count查詢的列名private String countColumn;// 排序private String orderBy;// 只增加排序private boolean orderByOnly;// sql攔截處理private BoundSqlInterceptor boundSqlInterceptor;// 分頁實現類,可以使用 {@link com.github.pagehelper.page.PageAutoDialect} 類中注冊的別名,例如 "mysql", "oracle"private String dialectClass;// 轉換count查詢時保留查詢的 order by 排序private Boolean keepOrderBy;// 轉換count查詢時保留子查詢的 order by 排序private Boolean keepSubSelectOrderBy;// 異步count查詢private Boolean asyncCount;
}
方法 | 默認值 | 簡要說明 |
---|---|---|
List getResult() | emptyList | 獲取分頁后的數據列表 |
Long getTotal() | 0 | 獲取列表總記錄數 |
int getPageNum() | 1 | 獲取當前頁碼 |
int getPageSize() | 10 | 每頁顯示條數,默認 10 |
boolean isLastPage() | 判斷是否為第一頁 | |
boolean isLastPage() | 判斷是否為最后一頁 | |
boolean hasPreviousPage() | 判斷是否有上一頁 | |
boolean hasNextPage() | 判斷是否有下一頁 | |
int getPrePage() | 獲取上一頁的頁碼。 | |
int getNextPage() | 獲取下一頁的頁碼。 |
分頁信息封裝類 PageInfo
??我們在使用 MyBatis 分頁時,不論是使用自動分頁還是手動分頁都會使用 PageInfo 對象作為承接介質。PageInfo 是 PageHelper 提供的用于封裝分頁結果的對象,包含完整的分頁信息。它可以將分頁查詢結果和分頁參數封裝在一個對象中,便于傳輸和使用。
public class PageInfo<T> extends PageSerializable<T> {// 當前頁private int pageNum;// 每頁的數量private int pageSize;// 當前頁的數量private int size;// 當前頁面第一個元素在數據庫中的行號(不常用)private long startRow;// 當前頁面最后一個元素在數據庫中的行號(不常用)private long endRow;// 總頁數private int pages;// 前一頁private int prePage;// 下一頁private int nextPage;// 是否為第一頁private boolean isFirstPage = false;// 是否為最后一頁private boolean isLastPage = false;// 是否有前一頁private boolean hasPreviousPage = false;// 是否有下一頁private boolean hasNextPage = false;// 導航頁碼數private int navigatePages;// 所有導航頁號private int[] navigatepageNums;// 導航條上的第一頁private int navigateFirstPage;// 導航條上的最后一頁private int navigateLastPage;
??PageHelper 提供了豐富的存儲和管理分頁相關的參數配置選項,例如:
public PageInfo<User> getUsers(int pageNum, int pageSize, String orderBy) {PageHelper.startPage(pageNum, pageSize).setOrderBy(orderBy);List<User> userList = userMapper.selectUsers(null); return new PageInfo<>(userList);
}
??PageInfo 作為后端返回給前端的標準分頁數據格式,便于前端渲染分頁組件。它不僅包含了分頁數據,還提供了更多的輔助信息,如是否為第一頁、最后一頁、導航頁碼等,前端在使用時無需手動計算分頁參數,提高可讀性和可維護性。
public void print PageInfo pageInfo) {System.out.println(" 當前頁碼:" + pageInfo.getPageNum()); System.out.println(" 總記錄數:" + pageInfo.getTotal()); System.out.println(" 總頁數:" + pageInfo.getPages());
}
四、PageHelper 實現原理
4.1 使用 ThreadLocal 記錄分頁參數
在調用 startPage
方法時,會通過 ThreadLocal 存儲當前分頁參數:
public static <E> Page<E> startPage(int pageNum, int pageSize, boolean count, Boolean reasonable, Boolean pageSizeZero) {Page<E> page = new Page<E>(pageNum, pageSize, count);page.setReasonable(reasonable);page.setPageSizeZero(pageSizeZero);//當已經執行過orderBy的時候Page<E> oldPage = getLocalPage();if (oldPage != null && oldPage.isOrderByOnly()) {page.setOrderBy(oldPage.getOrderBy());}//設置ThreadLocalsetLocalPage(page);return page;
}
附錄
分頁插件參數
分頁插件提供了多個可選參數,這些參數使用時,按照上面配置方式中的示例配置即可。分頁插件可選參數如下表所示:
屬性 | 簡要說明 |
---|---|
Boolean offsetAsPageNum | 通常用于指定是否將傳入的 offset 當作 pageNum 頁碼使用。 因為 PageHelper 默認使用頁碼(pageNum)、每頁記錄數(pageSize)來進行分頁的。 |
Boolean rowBoundsWithCount | 用于指定是否進行 count 查詢以獲取總記錄數。 設置為 true 表示 PageHelper 在執行分頁查詢時,會先執行一個 count 查詢來獲取總記錄數。 |
Boolean pageSizeZero | |
Boolean reasonable | |
Boolean supportMethodsArguments | |
String dialect | 不同數據庫 SQL 語句不同,指定了數據庫方言為 Mysql。 |
String helperDialect | 指定分頁插件使用哪種方言,候選值可參考 PageAutoDialect.java。 |
Boolean autoRuntimeDialect | 設置為 true 時,允許在運行時根據多數據源自動識別對應方言 |
Boolean autoDialect | |
Boolean closeConn | 通過該屬性來設置是否關閉獲取的這個連接,默認 true 關閉,設置為 false 后,不會關閉獲取的連接。 |
String params | |
Boolean defaultCount | 用于控制默認不帶 count 查詢的方法中,是否執行 count 查詢,默認 true 會執行 count 查詢。 |
String dialectAlias | 允許配置自定義實現的 別名,可以用于根據 JDBC URL 自動獲取對應實現,允許通過此種方式覆蓋已有的實現, |
String autoDialectClass | 用于自動獲取數據庫類型 |
String countColumn | 用于配置自動 count 查詢時的查詢列,默認值 0,也就是 count(0)。 |
String replaceSql | 可選值為 regex 和 simple,默認值空時采用 regex 方式 |
String sqlCacheClass | 針對 SQLServer 生成的 count 和 page sql 進行緩存 |
String boundSqlInterceptors | |
Boolean keepOrderBy | 轉換count查詢時保留查詢的 order by 排序。 |
Boolean keepSubSelectOrderBy | 轉換count查詢時保留子查詢的 order by 排序。 |
Boolean asyncCount | |
String countSqlParser | |
String orderBySqlParser | |
String sqlServerSqlParser |
常見問題及解決方法
分頁上下文未清理導致干擾
??在同一個查詢操作中,如果多次調用 PageHelper.startPage()
方法,可能會導致分頁參數被覆蓋或產生不可預期的結果。
/*** 第一次分頁查詢*/
PageHelper.startPage(1, 10);
List<DataA> listA = mapperA.queryDataA();/*** 第二次分頁查詢*/
PageHelper.startPage(2, 5);
List<DataB> listB = mapperB.queryDataB();// 結果可能被第二次分頁干擾
PageInfo<DataA> pageInfoA = new PageInfo<>(listA);
??對此,在每次分頁查詢后,調用 PageHelper.clearPage()
清理上下文。
PageHelper.startPage(1, 10);
List<DataA> listA = mapperA.queryDataA();
PageInfo<DataA> pageInfoA = new PageInfo<>(listA);
// 清理上下文
PageHelper.clearPage();PageHelper.startPage(2, 5);
List<DataB> listB = mapperB.queryDataB();
PageInfo<DataB> pageInfoB = new PageInfo<>(listB);
PageHelper.clearPage();
查詢未執行導致上下文污染
??如果分頁查詢代碼在條件分支中,而分支未被執行,分頁上下文未被清理會干擾后續查詢。
PageHelper.startPage(1, 10);
if (someCondition) {List<Data> list = mapper.queryData(); // 查詢未執行
}
// 后續查詢會被干擾
??對此,可以將分頁查詢代碼移到條件分支內部,確保分頁邏輯與查詢一一對應。或者在條件分支中調用 PageHelper.clearPage()
清理上下文。
總結
??通過本文的學習,你已經掌握了 PageHelper 的核心概念、使用方法和實際應用。從它的起源到現代應用,再到具體的代碼實現和最佳實踐,每一個環節都進行了詳細的講解。未來,隨著 MyBatis 和 PageHelper 的不斷發展,分頁查詢的功能會越來越強大。通過正確理解和使用 PageHelper,開發者可以高效完成分頁需求,同時規避潛在問題,提升系統性能與穩定性,寫出更加高效、優雅的代碼!