存儲過程需接收**表名參數**,查詢該表資料並處理。靜態游標無法使用變數作為表名。
CREATE OR REPLACE PROCEDURE process_table(p_table_name IN VARCHAR2) IS
TYPE ref_cur IS REF CURSOR; -- 弱類型 REF CURSOR
c_cur ref_cur;
v_id NUMBER;
v_sum NUMBER := 0;
BEGIN
-- 動態開啟游標:表名串接進 SQL 字串
OPEN c_cur FOR 'SELECT employee_id FROM ' || p_table_name;
LOOP
FETCH c_cur INTO v_id;
EXIT WHEN c_cur%NOTFOUND;
v_sum := v_sum + v_id;
END LOOP;
CLOSE c_cur;
DBMS_OUTPUT.PUT_LINE('Total: ' || v_sum);
END;
/
BEGIN
process_table('employees');
process_table('emp_archive');
END;
/
-- 定義強類型游標 (回傳結構固定)
TYPE emp_cur IS REF CURSOR RETURN employees%ROWTYPE;
CREATE OR REPLACE PROCEDURE get_emp_data(p_table_name IN VARCHAR2, p_cur OUT emp_cur) IS
BEGIN
-- 僅限回傳結構符合 employees%ROWTYPE 的表
OPEN p_cur FOR 'SELECT * FROM ' || p_table_name;
END;
/
CREATE OR REPLACE PROCEDURE copy_table_data(
p_source_table IN VARCHAR2,
p_target_table IN VARCHAR2,
p_where_clause IN VARCHAR2 DEFAULT NULL
) IS
TYPE ref_cur IS REF CURSOR;
c_src ref_cur;
v_sql VARCHAR2(4000);
v_cols VARCHAR2(4000);
BEGIN
-- 1. 取得欄位清單 (排除系統欄位)
SELECT LISTAGG(column_name, ',') WITHIN GROUP (ORDER BY column_id)
INTO v_cols
FROM user_tab_columns
WHERE table_name = UPPER(p_source_table)
AND column_name NOT IN ('CREATED_BY','CREATED_DATE','UPDATED_BY','UPDATED_DATE');
-- 2. 建構動態查詢
v_sql := 'SELECT ' || v_cols || ' FROM ' || p_source_table;
IF p_where_clause IS NOT NULL THEN
v_sql := v_sql || ' WHERE ' || p_where_clause;
END IF;
-- 3. 開啟來源游標
OPEN c_src FOR v_sql;
-- 4. 批次寫入目標表 (使用 FORALL 效能最佳)
-- 這裡簡化為逐行,實際建議 BULK COLLECT + FORALL
LOOP
-- 動態 FETCH 需要 DBMS_SQL 或預知結構
-- 實務上建議:目標表結構固定,來源表結構相同
NULL; -- 實作略
END LOOP;
CLOSE c_src;
EXCEPTION
WHEN OTHERS THEN
IF c_src%ISOPEN THEN CLOSE c_src; END IF;
RAISE;
END;
/
動態串接表名極易遭 SQL 注入,必須嚴格驗證:
-- 驗證表名合法性
FUNCTION is_valid_table(p_name VARCHAR2) RETURN BOOLEAN IS
v_cnt NUMBER;
BEGIN
SELECT COUNT(*) INTO v_cnt
FROM user_tables
WHERE table_name = UPPER(p_name);
RETURN v_cnt = 1;
END;
-- 在程序開頭檢查
IF NOT is_valid_table(p_table_name) THEN
RAISE_APPLICATION_ERROR(-20001, 'Invalid table name: ' || p_table_name);
END IF;
| 方案 | 優點 | 缺點 | 適用場景 |
|---|---|---|---|
| REF CURSOR + OPEN FOR | 標準、支援動態表名、可回傳客戶端 | 需防注入、FETCH 需知結構 | 報表、ETL、動態查詢 |
| DBMS_SQL | 完全動態 (欄位也動態)、可描述結果集 | API 複雜、效能較低 | 完全未知結構的動態 SQL |
| EXECUTE IMMEDIATE BULK COLLECT | 簡潔、效能好 | 需知結構、無法回傳游標 | 內部處理、不回傳客戶端 |
| 多態視圖 / 繼承表 | 靜態 SQL、最佳效能 | 結構固定、維護成本高 | 固定數量的相似表 |
| 分區表 | 單一表名、自動路由、效能最佳 | 需規劃分區鍵 | 大量資料、按時間/範圍分區 |
CREATE OR REPLACE PROCEDURE describe_and_fetch(p_table_name IN VARCHAR2) IS
v_cursor INTEGER := DBMS_SQL.OPEN_CURSOR;
v_col_cnt INTEGER;
v_desc DBMS_SQL.DESC_TAB;
v_sql VARCHAR2(4000) := 'SELECT * FROM ' || p_table_name;
BEGIN
-- 驗證表名 (略)
DBMS_SQL.PARSE(v_cursor, v_sql, DBMS_SQL.NATIVE);
DBMS_SQL.DESCRIBE_COLUMNS(v_cursor, v_col_cnt, v_desc);
-- 動態定義欄位
FOR i IN 1..v_col_cnt LOOP
DBMS_SQL.DEFINE_COLUMN(v_cursor, i, '', 4000); -- 簡化為 VARCHAR2
END LOOP;
IF DBMS_SQL.EXECUTE(v_cursor) > 0 THEN
LOOP
EXIT WHEN DBMS_SQL.FETCH_ROWS(v_cursor) = 0;
FOR i IN 1..v_col_cnt LOOP
DBMS_SQL.COLUMN_VALUE(v_cursor, i, v_val);
DBMS_OUTPUT.PUT(v_desc(i).col_name || '=' || v_val || ' | ');
END LOOP;
DBMS_OUTPUT.NEW_LINE;
END LOOP;
END IF;
DBMS_SQL.CLOSE_CURSOR(v_cursor);
EXCEPTION
WHEN OTHERS THEN
IF DBMS_SQL.IS_OPEN(v_cursor) THEN DBMS_SQL.CLOSE_CURSOR(v_cursor); END IF;
RAISE;
END;
/
USER_TABLES 確認存在