我可以迭代复合类型的列吗?

时间:2015-09-02 12:52:25

标签: oracle plsql

假设我有以下名为bar的表:

key    | columnA   | columnB   | columnC
A      | B         | C         | D
E      | F         | G         | H

我想编写一个带有键和字符串的函数并执行以下操作(最好用示例描述):

  • 输入:('A''${columnB} - ${columnA}')/输出:'C - B'
  • 输入:('B''Hello ${columnC}')/输出:'Hello H'

目前,我有这个实现:

CREATE OR REPLACE FUNCTION foo
( param_key IN VARCHAR2
, format_string IN VARCHAR2
)
RETURN VARCHAR2
IS
    my_row bar%ROWTYPE;
    retval VARCHAR2(4000);
BEGIN
    BEGIN SELECT * INTO my_row FROM bar WHERE "key" = param_key;
    EXCEPTION WHEN NO_DATA_FOUND THEN RETURN NULL;
    END;

    retval := format_string;
    retval := REPLACE(retval, '${columnA}', my_row.columnA);
    retval := REPLACE(retval, '${columnB}', my_row.columnB);
    retval := REPLACE(retval, '${columnC}', my_row.columnC);

    RETURN retval;
END;
/

我想避免在最后一部分逐个枚举所有列,因为我的表的结构可以改变(例如新列)。有没有办法迭代my_row的所有列,并以通用方式将${the column name}替换为该列中存储的值?

谢谢

3 个答案:

答案 0 :(得分:3)

实现这一目标的另一种方法 从表行创建xmltype。
从format_string创建xsl-transform 使用xsl转换xml

declare 
v_string_format varchar2(200) := '{columnA} + {columnB} + {columnA}{columnB}';
v_key varchar2(10) := 'A';
v_cursor sys_refcursor; 
l_xml xmltype;
v_xslt VARCHAR2(500):='<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"><xsl:template match="/ROWSET/ROW">{patern}</xsl:template></xsl:stylesheet>';
begin 
-- create xsl transform
v_string_format :=  upper(v_string_format);
v_string_format := REPLACE(v_string_format,'{','<xsl:value-of select="');
v_string_format := REPLACE(v_string_format,'}','"/>');
v_xslt := replace(v_xslt,'{patern}',v_string_format);
dbms_output.put_line(v_string_format);
-- open cursor for table
 open v_cursor for select * from bar where key = v_key;
-- get v_cursor as xmltype. 
 l_xml := xmltype(v_cursor);
 -- print xml
 dbms_output.put_line(l_xml.getClobVal());
 -- tranform xml and print result 
 dbms_output.put_line(l_xml.transform(xmltype(v_xslt)).getClobVal());
 close v_cursor;
end; 

答案 1 :(得分:2)

您可以在使用动态查询后获得结果......

CREATE OR REPLACE FUNCTION foo
( param_key IN VARCHAR2
, format_string IN VARCHAR2
)
RETURN VARCHAR2
IS
    retval VARCHAR2(4000) := format_string;
    cols SYS.ODCIVARCHAR2LIST;
BEGIN
    SELECT  COLUMN_NAME
    BULK COLLECT INTO cols
    FROM    USER_TAB_COLUMNS
    WHERE   TABLE_NAME = 'bar'
    ORDER BY COLUMN_ID;

    FOR i IN 1 .. cols.COUNT LOOP
      EXECUTE IMMEDIATE 'SELECT REPLACE( :1, ''${' || cols(i) || '}'', ' || cols(i) || ' ) FROM bar WHERE key = :2'
      INTO retval
      USING retval, param_key;
    END LOOP;

    RETURN retval;
EXCEPTION
  WHEN NO_DATA_FOUND THEN
    RETURN NULL;
END;
/

......但是:

  • 这使用动态SQL直接查询表,不使用%ROWTYPE记录。
  • 您可能无权访问USER_TAB_COLUMNS(或可能需要ALL_TAB_COLUMNS),而DBA可能不希望您有权访问数据字典表。
  • 可能(几乎可以肯定)非常低效。
  • 之前我已经看过这件事并且从未让它通过代码审查(写出明确的列名似乎总是更好)。

所以,虽然有可能,但我会说不要这样做。

答案 2 :(得分:2)

这是一个更有效的解决方案。当然,您必须编写更多代码,并使用动态SQL的全部范围。

CREATE OR REPLACE FUNCTION foo (param_key IN VARCHAR2, format_string IN VARCHAR2) 
RETURN VARCHAR2 IS

    retval VARCHAR2(4000) := format_string;

    cur SYS_REFCURSOR;
    curId  INTEGER;

    descTab DBMS_SQL.DESC_TAB;
    colCnt NUMBER;

    numvar NUMBER;
    datevar DATE;
    namevar VARCHAR2(4000);
    tsvar TIMESTAMP;

BEGIN

    OPEN cur FOR SELECT * FROM bar WHERE "key" = param_key;
    curId := DBMS_SQL.TO_CURSOR_NUMBER(cur);

    DBMS_SQL.DESCRIBE_COLUMNS(curId, colCnt, descTab);
    -- Define columns
    FOR i IN 1..colcnt LOOP
        IF desctab(i).col_type = DBMS_TYPES.TYPECODE_NUMBER THEN
            DBMS_SQL.DEFINE_COLUMN(curid, i, numvar);
        ELSIF desctab(i).col_type = DBMS_TYPES.TYPECODE_DATE THEN
            DBMS_SQL.DEFINE_COLUMN(curid, i, datevar);
        ELSIF desctab(i).col_type = DBMS_TYPES.TYPECODE_TIMESTAMP THEN
            DBMS_SQL.DEFINE_COLUMN(curid, i, tsvar);
        ELSIF desctab(i).col_type = DBMS_TYPES.TYPECODE_VARCHAR2 THEN
            DBMS_SQL.DEFINE_COLUMN(curid, i, namevar, 4000);
        --ELSIF desctab(i).col_type = ... THEN
            --DBMS_SQL.DEFINE_COLUMN(curid, i, ...);    
        END IF;
    END LOOP;

    -- Fetch Rows
    IF DBMS_SQL.FETCH_ROWS(curid) > 0 THEN
        -- Fetch only the first row and do not consider if further rows exist, 
        -- otherwise use WHILE DBMS_SQL.FETCH_ROWS(curid) > 0 LOOP
        FOR i IN 1..colcnt LOOP
            IF desctab(i).col_type = DBMS_TYPES.TYPECODE_VARCHAR2 THEN
                DBMS_SQL.COLUMN_VALUE(curid, i, namevar);
                retval := REPLACE(retval, '${'||desctab(i).col_name||'}', namevar);
            ELSIF desctab(i).col_type = DBMS_TYPES.TYPECODE_NUMBER THEN
                DBMS_SQL.COLUMN_VALUE(curid, i, numvar);
                retval := REPLACE(retval, '${'||desctab(i).col_name||'}', numvar);
            ELSIF desctab(i).col_type = DBMS_TYPES.TYPECODE_DATE THEN
                DBMS_SQL.COLUMN_VALUE(curid, i, datevar);
                retval := REPLACE(retval, '${'||desctab(i).col_name||'}', datevar);
            ELSIF desctab(i).col_type = DBMS_TYPES.TYPECODE_TIMESTAMP THEN
                DBMS_SQL.COLUMN_VALUE(curid, i, tsvar);
                retval := REPLACE(retval, '${'||desctab(i).col_name||'}', tsvar);
            --ELSIF desctab(i).col_type = ... THEN
                --DBMS_SQL.COLUMN_VALUE(curid, i, ...);
                --retval := REPLACE(retval, '${'||desctab(i).col_name||'}', ...);           
            END IF;
        END LOOP;
    ELSE
        retval := NULL;
    END IF;
    DBMS_SQL.CLOSE_CURSOR(curId);

    RETURN retval;  

END;