import java.util.List;
import org.hibernate.HibernateException;
import org.hibernate.Query;
import org.hibernate.ScrollableResults;
public class Page {
private List result; // 當前頁數據
private int pageSize; // 頁大小
private int startPage; // 起始頁碼 (1-based)
private ScrollableResults scrollableResults;
private int totalResults; // 總記錄數
private int totalPages; // 總頁數
public Page(int startPage, int pageSize, Query query) {
this.startPage = startPage;
this.pageSize = pageSize;
try {
// 1. 獲取可滾動結果集
this.scrollableResults = query.scroll();
// 2. 定位末尾獲取總行數
this.scrollableResults.last();
if (scrollableResults.getRowNumber() >= 0) {
this.totalResults = this.scrollableResults.getRowNumber() + 1; // 0-based → count
} else {
this.totalResults = 0;
}
setTotalPages();
// 3. 獲取當前頁數據 (setFirstResult 0-based)
result = query.setFirstResult((this.getStartPage() - 1) * this.pageSize)
.setMaxResults(this.pageSize).list();
} catch (HibernateException e) {
e.printStackTrace();
}
}
// Getter 方法...
public List getResult() { return result; }
public int getStartPage() {
if (startPage < 1) startPage = 1;
if (startPage > totalPages) startPage = totalPages;
return startPage;
}
public int getTotalResults() { return totalResults; }
public int getPageSize() { return pageSize; }
public boolean isFirstPage() { return this.startPage == 1; }
public boolean hasNextPage() { return this.startPage < this.totalPages; }
public boolean hasPreviousPage() { return this.startPage > 1; }
public int getTotalPages() { return totalPages; }
private void setTotalPages() {
this.totalPages = this.totalResults / this.pageSize;
if (totalPages * pageSize < totalResults) totalPages++;
}
}
scroll().last() 移動到結果集末尾,getRowNumber() 返回 0-based 索引,+1 得到總數。避免了單獨 SELECT COUNT(*) 查詢。scroll() 獲取總數,再 setFirstResult/setMaxResults 獲取分頁數據。**兩次完整查詢執行**,性能開銷大。startPage 為 1-based (用戶視角),內部 setFirstResult 需 0-based,轉換公式 (page-1)*size。getStartPage() 自動修正越界頁碼至首頁/末頁。| 問題 | 影響 | 優化方案 |
|---|---|---|
| 雙重查詢執行 | 大數據量下雙倍資源消耗,響應時間翻倍 | 1. 使用 query.setMaxResults(0).scroll() 僅獲取總數2. 或單獨執行 SELECT COUNT(*) (數據庫優化器通常更快)3. Hibernate 3.2+ 支持 query.setReadOnly(true) 減少快照開銷 |
| ScrollableResults 資源洩漏風險 | 未顯式關閉 scrollableResults,長事務/連接池洩漏 | 實現 Closeable,finally 塊調用 scrollableResults.close() |
| 異常處理僅打印堆棧 | 調用方無法感知失敗,返回空結果掩蓋錯誤 | 拋出自定義 PaginationException 或返回 Result<Page> 包裝 |
| 無緩存支持 | 相同查詢重複分頁重複計算總數 | 引入二級緩存緩存總數,或應用層緩存 Page 對象 |
| 線程不安全 | 實例字段 scrollableResults 非線程安全 | 確保 Page 為請求作用域,勿作為單例/會話屬性 |
| 鎖定 Hibernate API | 難以遷移至 JPA/JdbcTemplate/MyBatis | 提取 PaginationProvider 接口,實現 Hibernate/JPA 多適配器 |
// 1. Spring Data JPA (推薦)
Pageable pageable = PageRequest.of(page-1, size, Sort.by("id"));
Page page = repository.findAll(pageable); // 自動 COUNT + 分頁
// 2. MyBatis PageHelper
PageHelper.startPage(page, size);
List list = mapper.selectAll();
PageInfo pageInfo = new PageInfo<>(list);
// 3. Hibernate 5.2+ 原生
Query query = session.createQuery("from Entity", Entity.class);
query.setFirstResult((page-1)*size).setMaxResults(size);
List list = query.list();
// 總數需單獨查詢或使用 ScrollableResults (同上)
// 4. JPA 標準
TypedQuery q = em.createQuery("SELECT e FROM Entity e", Entity.class);
q.setFirstResult((page-1)*size).setMaxResults(size);
Long total = (Long) em.createQuery("SELECT COUNT(e) FROM Entity e").getSingleResult();
COUNT(*) + 分頁查詢雙語句,或框架內建分頁ScrollMode.SCROLL_SENSITIVE 可能持有數據庫鎖,僅只讀事務中短時間使用page >= 1、size <= 1000 (防止惡意大頁碼導致全表掃描){data:[], total:, page:, size:, pages:} 前端通用消費此 Page 類展示了早期 Hibernate 手工分頁的典型實現:**ScrollableResults 獲取總數 + 雙重查詢 + 1-based/0-based 轉換**。雖然邏輯完整,但雙重查詢、資源管理、異常處理等工程化缺失明顯。現代項目應優先使用 **Spring Data JPA / MyBatis PageHelper / Hibernate 6 內建分頁**,僅在極簡環境或學習目的下參考此實現。