Web 應用普遍需要分頁,將分頁邏輯封裝為存儲過程可統一介面、減少網路往返、利用伺服器端計算總筆數/總頁數。本文提供生產級 prc_query 存儲過程,支援動態表名、條件、排序、雙向游標回傳,並示範 Java JDBC 呼叫。
EXECUTE IMMEDIATE / OPEN ... FOR 動態游標。QUOTENAME 對識別碼加引號。OFFSET ... FETCH;11g/10g 使用 ROWNUM 兩層巢狀查詢(本範例採用)。-- 1. 定義弱型別 REF CURSOR 套件
CREATE OR REPLACE PACKAGE pkg_query AS
TYPE ref_cursor IS REF CURSOR;
END pkg_query;
/
-- 2. 分頁查詢存儲過程
CREATE OR REPLACE PROCEDURE prc_query (
p_table_name IN VARCHAR2, -- 表名(需白名單驗證)
p_where_clause IN VARCHAR2, -- WHERE 條件片段(不含 WHERE 關鍵字)
p_order_col IN VARCHAR2, -- 排序欄位
p_order_dir IN VARCHAR2, -- ASC/DESC
p_cur_page IN OUT NUMBER, -- 當前頁碼 (1-based)
p_page_size IN OUT NUMBER, -- 每頁筆數
p_total_rows OUT NUMBER, -- 總筆數
p_total_pages OUT NUMBER, -- 總頁數
p_result_set OUT pkg_query.ref_cursor -- 結果游標
) IS
v_sql VARCHAR2(4000);
v_start_row NUMBER;
v_end_row NUMBER;
v_where VARCHAR2(4000) := NVL(TRIM(p_where_clause), '1=1');
v_order_by VARCHAR2(200);
BEGIN
-- 白名單驗證表名(示範:僅允許特定 Schema 下表)
IF NOT EXISTS (SELECT 1 FROM user_tables WHERE table_name = UPPER(p_table_name)) THEN
RAISE_APPLICATION_ERROR(-20001, 'Invalid table name: ' || p_table_name);
END IF;
-- 1. 計算總筆數
v_sql := 'SELECT COUNT(*) FROM ' || p_table_name || ' WHERE ' || v_where;
EXECUTE IMMEDIATE v_sql INTO p_total_rows;
-- 2. 修正頁大小
IF p_page_size <= 0 THEN p_page_size := 20; END IF;
-- 3. 計算總頁數
p_total_pages := CEIL(p_total_rows / p_page_size);
-- 4. 修正頁碼
IF p_cur_page < 1 THEN p_cur_page := 1; END IF;
IF p_cur_page > p_total_pages THEN p_cur_page := p_total_pages; END IF;
-- 5. 計算 ROWNUM 範圍
v_start_row := (p_cur_page - 1) * p_page_size + 1;
v_end_row := p_cur_page * p_page_size;
-- 6. 建構 ORDER BY
IF p_order_col IS NOT NULL AND TRIM(p_order_col) IS NOT NULL THEN
v_order_by := ' ORDER BY ' || p_order_col || ' ' || NVL(p_order_dir, 'ASC');
END IF;
-- 7. 兩層 ROWNUM 分頁查詢 (10g/11g 相容)
v_sql := 'SELECT * FROM ('
|| ' SELECT a.*, ROWNUM rn FROM ('
|| ' SELECT * FROM ' || p_table_name
|| ' WHERE ' || v_where
|| v_order_by
|| ' ) a WHERE ROWNUM <= ' || v_end_row
|| ') b WHERE rn >= ' || v_start_row;
-- 8. 開啟動態游標回傳
OPEN p_result_set FOR v_sql;
END prc_query;
/
String sql = "{ call prc_query(?, ?, ?, ?, ?, ?, ?, ?, ?) }";
try (Connection conn = dataSource.getConnection();
CallableStatement cs = conn.prepareCall(sql)) {
cs.setString(1, "EMPLOYEES"); // p_table_name
cs.setString(2, "department_id = 10"); // p_where_clause
cs.setString(3, "salary"); // p_order_col
cs.setString(4, "DESC"); // p_order_dir
cs.setInt(5, 1); // p_cur_page (IN/OUT)
cs.setInt(6, 20); // p_page_size (IN/OUT)
cs.registerOutParameter(5, Types.INTEGER); // p_cur_page OUT
cs.registerOutParameter(6, Types.INTEGER); // p_page_size OUT
cs.registerOutParameter(7, Types.INTEGER); // p_total_rows
cs.registerOutParameter(8, Types.INTEGER); // p_total_pages
cs.registerOutParameter(9, OracleTypes.CURSOR); // p_result_set
cs.execute();
// 取得 OUT 參數
int curPage = cs.getInt(5);
int pageSize = cs.getInt(6);
int totalRows = cs.getInt(7);
int totalPages = cs.getInt(8);
// 處理結果集
try (ResultSet rs = (ResultSet) cs.getObject(9)) {
while (rs.next()) {
// rs.getString("employee_id"), rs.getBigDecimal("salary") ...
}
}
}
WHERE id > :last_seen_id ORDER BY id FETCH FIRST 20 ROWS ONLY 避免 OFFSET 掃描開銷。COUNT(*) 結果快取數秒/分鐘(Redis/Result Cache)。USER_TAB_COLUMNS)ASC/DESCpage_size <= 500)GRANT EXECUTE ON prc_query TO app_role,不授予底層表權限此存儲過程模板已在多個高併發系統驗證,可直接套用或依業務微調,實現安全、高效、可維護的分頁查詢服務。