首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

基於 Hibernate Query 實現通用分頁器:核心代碼解析與優化建議

     編輯:Java 相關知識

核心代碼

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++;
    }
}

關鍵機制解析

  1. ScrollableResults 獲取總數:scroll().last() 移動到結果集末尾,getRowNumber() 返回 0-based 索引,+1 得到總數。避免了單獨 SELECT COUNT(*) 查詢。
  2. 雙重查詢執行:構造函數中先 scroll() 獲取總數,再 setFirstResult/setMaxResults 獲取分頁數據。**兩次完整查詢執行**,性能開銷大。
  3. 頁碼轉換:構造參數 startPage 為 1-based (用戶視角),內部 setFirstResult 需 0-based,轉換公式 (page-1)*size。
  4. 邊界保護: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();

最佳實踐總結

  1. 生產環境禁用雙重查詢模式:改用 COUNT(*) + 分頁查詢雙語句,或框架內建分頁
  2. 總數緩存策略:低頻變更數據緩存總數 5-15 分鐘,高頻數據接受近似值
  3. 游標模式謹慎使用:ScrollMode.SCROLL_SENSITIVE 可能持有數據庫鎖,僅只讀事務中短時間使用
  4. 分頁參數驗證:page >= 1、size <= 1000 (防止惡意大頁碼導致全表掃描)
  5. 統一分頁響應格式:{data:[], total:, page:, size:, pages:} 前端通用消費

總結

此 Page 類展示了早期 Hibernate 手工分頁的典型實現:**ScrollableResults 獲取總數 + 雙重查詢 + 1-based/0-based 轉換**。雖然邏輯完整,但雙重查詢、資源管理、異常處理等工程化缺失明顯。現代項目應優先使用 **Spring Data JPA / MyBatis PageHelper / Hibernate 6 內建分頁**,僅在極簡環境或學習目的下參考此實現。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved