在傳統 Java EE 專案中,常將分頁邏輯下推至資料庫存儲過程,由 Oracle 負責查詢總筆數、計算總頁數、返回當頁游標。本範例展示如何封裝分頁參數物件 PageInfo,配合 Hibernate Session 獲取 JDBC Connection,呼叫 CallableStatement 執行存儲過程 pages.prc_page,並將 REF CURSOR 轉換為 List<Map> 返回。
package org.zh.basic;
public class PageInfo {
private String p_tableName; // 表名
private String p_strWhere; // WHERE 條件(不含 WHERE 關鍵字)
private String p_orderColumn; // 排序欄位
private String p_orderStyle; // ASC/DESC
private int p_curPage; // 當前頁(1-based)
private int p_pageSize; // 每頁筆數
private int p_totalRecords; // 總筆數(OUT)
private int p_totalPages; // 總頁數(OUT)
// 建構子、Getter/Setter 略...
}
package org.zh.sys.server;
import java.sql.*;
import java.util.*;
import org.hibernate.Session;
import org.zh.basic.PageInfo;
import org.zh.dao.HibernateSessionFactory;
import oracle.jdbc.OracleTypes;
import oracle.jdbc.OracleCallableStatement;
public class GeneratePage {
public static ArrayList<Map<String, Object>> Prc_Page(PageInfo page) {
ArrayList<Map<String, Object>> list = new ArrayList<>();
Session session = null;
Connection conn = null;
ResultSet rs = null;
CallableStatement proc = null;
try {
// 1. 從 Hibernate 獲取原生 Connection
session = HibernateSessionFactory.getSession();
conn = session.connection(); // 注意:Hibernate 5+ 建議用 session.doWork()
// 2. 準備呼叫:{call pages.prc_page(?,?,?,?,?,?,?,?,?)}
proc = conn.prepareCall("{call pages.prc_page(?,?,?,?,?,?,?,?,?)}");
proc.setString(1, page.getP_tableName());
proc.setString(2, page.getP_strWhere());
proc.setString(3, page.getP_orderColumn());
proc.setString(4, page.getP_orderStyle());
proc.setInt(5, page.getP_curPage());
proc.setInt(6, page.getP_pageSize());
// 3. 註冊 OUT 參數:總筆數、總頁數、游標
proc.registerOutParameter(7, OracleTypes.NUMBER);
proc.registerOutParameter(8, OracleTypes.NUMBER);
proc.registerOutParameter(9, OracleTypes.CURSOR);
// 4. 執行
proc.execute();
// 5. 取回 OUT 參數
page.setP_totalRecords(proc.getInt(7));
page.setP_totalPages(proc.getInt(8));
// 6. 取回 REF CURSOR 並轉換
rs = ((OracleCallableStatement) proc).getCursor(9);
ResultSetMetaData rsmd = rs.getMetaData();
int cols = rsmd.getColumnCount();
while (rs.next()) {
Map<String, Object> row = new HashMap<>(cols);
for (int i = 1; i <= cols; i++) {
row.put(rsmd.getColumnName(i), rs.getObject(i));
}
list.add(row);
}
return list;
} catch (SQLException | Exception e) {
e.printStackTrace();
return list;
} finally {
// 7. 資源關閉順序:ResultSet → CallableStatement → Connection
closeQuietly(rs);
closeQuietly(proc);
closeQuietly(conn);
// 注意:Session 由外層管理,此處不關閉
}
}
private static void closeQuietly(AutoCloseable c) {
if (c != null) try { c.close(); } catch (Exception ignored) {}
}
}
CREATE OR REPLACE PACKAGE pages AS
PROCEDURE prc_page(
p_tableName IN VARCHAR2,
p_strWhere IN VARCHAR2,
p_orderColumn IN VARCHAR2,
p_orderStyle IN VARCHAR2,
p_curPage IN NUMBER,
p_pageSize IN NUMBER,
p_totalRecords OUT NUMBER,
p_totalPages OUT NUMBER,
v_cur OUT SYS_REFCURSOR
);
END pages;
/
-- 過程體內部動態構建 SQL,執行 COUNT(*) 取總數,再用 ROWNUM/BETWEEN 或 OFFSET/FETCH 分頁
| 環節 | 說明 | 注意事項 |
|---|---|---|
| Connection 來源 | Hibernate session.connection() | Hibernate 5.3+ 棄用,建議 session.doWork(c -> ...) 或 session.unwrap(Session.class).connection() |
| CallableStatement | JDBC 標準呼叫存儲過程介面 | 使用 {call ...} 轉義語法,跨資料庫可攜性較好 |
| OUT 參數註冊 | registerOutParameter(idx, OracleTypes.NUMBER/CURSOR) | 必須使用 OracleTypes 常數,CURSOR 對應 SYS_REFCURSOR |
| REF CURSOR 獲取 | ((OracleCallableStatement)proc).getCursor(idx) | 需強轉 Oracle 實作類,綁定 Oracle 驅動 |
| ResultSet 轉 Map | ResultSetMetaData 取欄位名 + getObject | 通用但失去型別資訊,實際專案建議映射到 DTO/Entity |
| 資源釋放 | finally 區塊依序關閉 | 使用 try-with-resources (JDBC 4.1+) 更簡潔 |
SimpleJdbcCall 自動處理 OUT 參數、RowMapper 映射實體,程式碼量減少 80%。<select resultMap="..." statementType="CALLABLE">{call prc_page(...)}</select> 配合 resultType="map" 或自定義 ResultHandler。session.createNativeQuery("CALL prc_page(...)").setParameter(...).list()。OFFSET ... ROWS FETCH NEXT ... ROWS ONLY,可直接用 SQL 分頁,無需存儲過程。HibernateSessionFactory 整合 HikariCP/Druid,避免頻繁建立物理連線。本範例完整演示了「參數物件 → Hibernate Session → JDBC Connection → CallableStatement → REF CURSOR → List<Map>」的經典調用鏈路。雖現代框架已大幅簡化此流程,但理解底層 JDBC 機制對排查效能問題、處理複雜存儲過程、跨框架整合仍具核心價值。