在企業級 Java 應用開發中,數據分頁是極為常見的需求。早期 Hibernate 版本並未內建分頁支持,開發者往往需要自行封裝分頁邏輯。本文將深入剖析一套經典的分頁實現代碼,包含 Page 接口定義、HibernatePage 核心實現類以及輔助的 ListPage 實現,幫助讀者理解分頁機制的底層原理。
Page 接口定義了分頁對象應具備的標準行為,包含頁碼判斷、數據獲取、統計信息等核心方法:
isFirstPage()、isLastPage()、hasNextPage()、hasPreviousPage()getNextPageNumber()、getPreviousPageNumber()、getLastPageNumber()getThisPageFirstElementNumber()、getThisPageLastElementNumber()getThisPageElements()、getTotalNumberOfElements()、getPageSize()、getPageNumber()getLogger()接口採用零基礎頁碼(第一頁為 0),這與數組索引一致,便於底層計算。
類維護四個關鍵字段:
protected List elements; // 當前頁數據(含額外一條用於判斷是否有下一頁)
protected int pageSize; // 每頁大小
protected int pageNumber; // 當前頁碼(零基礎)
protected int totalElements; // 總記錄數
構造函數為 private,強制通過靜態工廠方法創建實例。
根據 JDBC 驅動是否支持可滾動游標,採用不同策略:
getScrollPageInstanceWithTotalByScroll(),通過 scrollableResults.last() 快速定位末尾獲取總行數,效率極高。getScrollPageInstanceWithTotalByList(),退而求其次通過 query.list().size() 計算總數,性能較差但兼容性好。驅動類名存儲於靜態集合 jdbcClassesSupportingScrollCursors 中,運行期動態判斷。
無論哪種模式,最終都調用此方法獲取當前頁數據:
elements = query.setFirstResult(this.pageNumber * this.pageSize)
.setMaxResults(this.pageSize + 1) // 多取一條判斷是否有下一頁
.list();
getThisPageElements() 會根據 hasNextPage() 截取前 pageSize 條,巧妙避免了額外的 count 查詢。
public int getLastPageNumber() {
double totalResults = new Integer(getTotalNumberOfElements()).doubleValue();
return new Double(Math.floor(totalResults / getPageSize())).intValue();
}
使用 Math.floor 配合零基礎頁碼,確保計算準確。例如 10 條記錄、頁大小 3,總頁數為 3(頁碼 0、1、2)。
適用於已將所有數據加載至內存的 List 場景:
List、頁碼、頁大小getTotalNumberOfElements() 返回 elements.size() - 1(預留一位)getThisPageElements() 通過 subList 直接截取,無需資料庫交互注意:此實現假設傳入列表已包含「額外一條」用於判斷邊界,使用時需注意協調。
LIMIT/OFFSET 或 ROW_NUMBER(),應優先使用 query.setFirstResult()/setMaxResults() 直推下推。calculateTotalElementsByList() 會導致 OOM,生產環境必須使用 SELECT COUNT(*) 或數據庫統計信息。ScrollMode.SCROLL_SENSITIVE 可能鎖表,長事務中慎用;分頁查詢建議使用只讀事務。這套代碼展示了早期 Hibernate 分頁的經典實現思路:雙模式適配、多取一條判斷邊界、零基礎頁碼內部計算。雖然現代框架已內建更優雅的分頁支持,但理解其核心邏輯對於排查性能問題、設計自定義分頁組件仍具參考價值。