发送显式 CURSOR 作为过程参数

Sendig explicit CURSOR as procedure parameter

我有以下代码。有一个具有 inner_procedure 的外部过程用于处理游标(将数据从游标连接到 m(消息)变量)。我可以打开游标并发送对内部处理过程的引用,例如:

PROCEDURE outer_proc AS

  m VARCHAR2(2000):='';

  cur SYS_REFCURSOR;

  PROCEDURE inner_proc(cur IN SYS_REFCURSOR,m OUT VARCHAR2) IS
    firstname VARCHAR2(20);
    lastname VARCHAR2(20);
  BEGIN
    LOOP
      FETCH cur INTO firstname,lastname;
      EXIT WHEN cur%NOTFOUND;
      m:=m||firstname||' '||lastname;
    END LOOP;
  END;

BEGIN

  OPEN cur FOR SELECT * FROM employees WHERE sallary<1000;
    inner_proc(cur,m);
  CLOSE cur;

  OPEN cur FOR SELECT * FROM employees WHERE sallary>=1000;
    inner_proc(cur,m);
  CLOSE cur;

END;     

但我想将显式游标名称发送到内部过程并让内部过程打开游标并像这样处理它:

PROCEDURE outer_proc AS

  TYPE cur_type IS REF CURSOR;

  m VARCHAR2(2000):='';

  CURSOR c1 IS SELECT * FROM employees WHERE sallary<1000;
  CURSOR c2 IS SELECT * FROM employees WHERE sallary>=1000;

  PROCEDURE inner_proc(cur IN cur_type,m OUT VARCHAR2) IS
    col1 VARCHAR2(20);
    col2 VARCHAR2(20);
  BEGIN
    OPEN cur;
      LOOP
        FETCH cur INTO col1,col2;
        EXIT WHEN cur%NOTFOUND;
        m:=m||col1||' '||col2;
      END LOOP;
    CLOSE cur;
  END;

BEGIN

  inner_proc(c1,m);
  inner_proc(c2,m);

END;     

在我上面的示例中,游标意外地具有相同的 %ROWTYPE,但 inner_procedure 无法提前知道。我的内部过程应该接收一个任意游标作为参数,打开它,最后将第一个和第二个游标的列连接到消息。

在我的第一个代码中,在参数中使用 SYS_REFCURSOR 我可能也会发送对不同游标的引用,所以...为什么我必须发送对已打开游标的引用,我想要打开的过程和关闭游标是内部程序的一部分。我想发送游标名称并将打开和关闭(当然是遍历)留给内部过程。

我怎样才能做到这一点?

我看不出有什么理由需要一个内部程序,你可以用一个独立的程序来实现它。当您将游标传递给 procedure/function 时,它必须是打开的,即您不能在内部过程中打开游标。

可能是这样的(未测试):

CREATE OR REPLACE FUNCTION process_cursor(cur IN SYS_REFCURSOR) AS VARCHAR2 IS
    res VARCHAR2(10000);

    curid INTEGER;
    col_cnt INTEGER;
    rec_tab DBMS_SQL.DESC_TAB;
    v_rows_processed INTEGER;

    col_1 VARCHAR2(4000);
    col_2 VARCHAR2(4000);

BEGIN
    curid := DBMS_SQL.TO_CURSOR_NUMBER(cur);
    DBMS_SQL.DESCRIBE_COLUMNS(curid, col_cnt, rec_tab);  

    -- The generic approach
    /*
    FOR c in 1..col_cnt LOOP
        rec := rec_tab(c);
        IF rec.col_type = DBMS_TYPES.TYPECODE_NUMBER THEN
            DBMS_SQL.DEFINE_COLUMN(curid, c, num_var); 
        ELSIF rec.col_type = DBMS_TYPES.TYPECODE_VARCHAR2 THEN
            DBMS_SQL.DEFINE_COLUMN(curid, c, string_var, rec.col_max_len); 
        ELSIF rec.col_type = DBMS_TYPES.TYPECODE_DATE THEN
            DBMS_SQL.DEFINE_COLUMN(curid, c, date_var); 
            -- .. some more data types if needed
        END IF;
    END LOOP;
    */

    -- however you like to get only the first two columns which are strings.
    DBMS_SQL.DEFINE_COLUMN(curid, 1, col_1, rec_tab(1).col_max_len);
    DBMS_SQL.DEFINE_COLUMN(curid, 2, col_2, rec_tab(2).col_max_len);

    v_rows_processed := DBMS_SQL.EXECUTE(curid);

    WHILE DBMS_SQL.FETCH_ROWS(curid) > 0 LOOP
        -- The generic approach
        /*
        FOR c IN 1..col_cnt LOOP
            rec := rec_tab(c);
            IF rec.col_type = DBMS_TYPES.TYPECODE_NUMBER THEN
                DBMS_SQL.COLUMN_VALUE(curid, c, num_var); 
            ELSIF rec.col_type = DBMS_TYPES.TYPECODE_VARCHAR2 THEN
                DBMS_SQL.COLUMN_VALUE(curid, c, string_var); 
            ELSIF rec.col_type = DBMS_TYPES.TYPECODE_DATE THEN
                DBMS_SQL.COLUMN_VALUE(curid, c, date_var); 
                -- .. some more data types if needed
            END IF;
        END LOOP;
        */

        -- however you like to get only the first two columns which are strings.
      res := res || DBMS_SQL.COLUMN_VALUE(curid, 1, col_1)||' '||DBMS_SQL.COLUMN_VALUE(curid, 2, col_2);
    END LOOP; 

    DBMS_SQL.CLOSE_CURSOR(curid);
    RETURN res;

END;

如果过程只有一个 return 值,我更喜欢 FUNCTION 而不是 PROCEDURE。

您可以这样调用该函数:

DECLARE
    cur SYS_REFCURSOR;
    ret VARCHAR2(10000);
BEGIN
    OPEN cur FOR SELECT * FROM employees WHERE salary < 1000;
    ret := process_cursor(cur);

    OPEN cur FOR SELECT * FROM customers WHERE turnover > 1000;
    ret := process_cursor(cur);

END;

如果您坚持在函数内打开游标,则必须将查询作为字符串传递。可能是这样的:

CREATE OR REPLACE FUNCTION process_cursor_str(sqlStr IN VARCHAR2) AS VARCHAR2 IS

    cur SYS_REFCURSOR;
    res VARCHAR2(10000);
    ...

BEGIN
    OPEN cur FOR sqlStr;
    curid := DBMS_SQL.TO_CURSOR_NUMBER(cur);
    ...


DECLARE
    ret VARCHAR2(10000);
BEGIN
    ret := process_cursor_str('SELECT * FROM employees WHERE salary < 1000');       
    ret := process_cursor_str('SELECT * FROM customers WHERE turnover > 1000');    
END;

From the documentation on explicit cursors(强调已加):

You cannot assign a value to an explicit cursor, use it in an expression, or use it as a formal subprogram parameter or host variable. You can do those things with a cursor variable (see "Cursor Variables").

因此您不能发送显式游标,在您的示例中 c1c2 - 您不能有采用显式游标参数的过程。

您可以提供 ref 游标作为 formal parameter,正如您已经在做的那样。就像您一样,那可能是一个打开的游标;或您随后打开的游标变量。但是您似乎真的不希望将光标放在过程调用之外,因此两者似乎都不是您想要的。

如果您定义了一个小的游标列表,您可以直接将游标的 name 作为字符串传递,然后在过程中使用 case 语句打开相关的预定义游标:

PROCEDURE outer_proc AS

  m VARCHAR2(2000):='';

  CURSOR c1 IS SELECT first_name,last_name FROM employees WHERE salary<1000;
  CURSOR c2 IS SELECT first_name,last_name FROM employees WHERE salary>=1000;

  PROCEDURE inner_proc(cur_name IN varchar2,m OUT VARCHAR2) IS
    col1 VARCHAR2(20);
    col2 VARCHAR2(20);
  BEGIN
    CASE cur_name
      WHEN 'c1' THEN
        OPEN c1;
        LOOP
          FETCH c1 INTO col1,col2; -- this will only work in the cursor only selects two columns!
          EXIT WHEN c1%NOTFOUND;
          m:=m||col1||' '||col2;
        END LOOP;
        CLOSE c1;
      WHEN 'c2' THEN
        OPEN c2;
        LOOP
          FETCH c2 INTO col1,col2; -- this will only work in the cursor only selects two columns!
          EXIT WHEN c2%NOTFOUND;
          m:=m||col1||' '||col2;
        END LOOP;
        CLOSE c2;
    END CASE;
  END;

BEGIN

  inner_proc('c1',m);
  inner_proc('c2',m);

END;     
/

...但这不可扩展,而且可以说已经比你现在拥有的更糟糕了。

或者将游标查询,而不是游标,传入过程,然后用动态SQL打开并处理。正如@Wernfried 所展示的那样,我不会详细介绍。不过,这样做的可能缺点是,如果您已经有一个要尝试重用的显式游标,那么您现在正在复制该查询。哦,由于它是动态的,查询本身要到 运行 时间才会被解析,因此可能不会像您希望的那样尽早发现错误。

TL;DR - 参见 *8-)