1. 程式人生 > >Oracle PL/SQL進階程式設計(第十五彈:動態SQL語句)

Oracle PL/SQL進階程式設計(第十五彈:動態SQL語句)

理解動態SQL語句

動態SQL語句基礎

動態SQL語句不僅是指SQL語句是動態拼接而成的,更主要的是SQL語句所使用的物件也是執行時期才建立的。出現這種功能跟PL/SQL本身的早起繫結特性有關,早PL/SQL中,所有的物件必須已經存在於資料庫中才能執行,比如要查詢emp表,emp表必須已經存在,否則會報錯。此時可以通過動態SQL,因為動態SQL不被PL/SQL引擎編譯時分析,而是在執行時進行分析並執行。

雖然動態SQL語句可以讓我們在執行時動態地切換表名或欄位名,以及在PL/SQL中執行DDL語句,但是在如下方面仍然不及靜態SQL語句方便:
- 靜態SQL在編譯或測試時,可以立即知道對錯,比如物件是否存在,許可權是否具備,而動態SQL要在執行時才知道。
- 使用靜態SQL時,可以對要執行的SQL進行效能優化調整,動態SQL不具備這種能力。

動態SQL使用時機

舉個例子,我們經常會需要臨時儲存中間資料,因此會先檢測目標表是否存在,如果存在則插入資料,如果不存在則先建立表,再插入資料。
如果我們在PL/SQL程式碼中直接用CREATE TABLE,會報錯,所以必須把CREATE TABLE語句使用動態SQL來執行:
EXECUTE IMMEDIATE 'CREATE TABLE ...';

下面是使用動態SQL的幾個時機:
- 由於在PL/SQL中只能執行靜態的查詢和DML語句,因此如果 要執行DDL語句,必須藉助動態SQL。
- 在開發報表或一些複雜的應用程式邏輯時,如果要基於引數化的查詢方式,比如動態的表字段和動態的表名稱,可以使用動態SQL。
- 基於資料表儲存業務規則和軟體程式碼,可以將很多的業務規則的程式碼寫在一個表的記錄中,在程式需要時檢索不同的業務邏輯的程式碼動態地執行。

從Oracle 7開始,可以使用DBMS_SQL包來動態執行動態SQL語句,在Oracle 8i之後,Oracle提供了執行動態SQL語句的另外一個選擇:本地動態SQL(NDS)。NDS是PL/SQL原生部分,比使用DBMS_SQL更簡單更方便,它僅提供了一個名為EXECUTE IMMEDIATE的過程。

本地動態SQL

本地動態SQL縮寫為NDS,全稱是Native Dynamic SQL。NDS提供了比DBMS_SQL更簡單的語法,但是NDS不支援事先不知道引數的個數、名稱或資料型別的動態SQL語句,此時需要使用DBMS_SQL來解決。

可以使用如下3種不同型別的動態方法使用本地動態SQL:
- EXECUTE IMMEDIATE:該語句可以處理多數動態SQL操作,包括DDL語句,比如CREATE、ALTER、DROP等;DCL語句,比如GRANT、REVOKE等;DML語句,比如INSERT、UPDATE、DELETE等,以及單行的SELECT語句。不能使用EXECUTE IMMEDIATE來處理多行的查詢語句,多行查詢需要用OPEN FOR。
- 使用OPEN FOR、FETCH和CLOSE語句執行多行查詢。
- 使用批量SQL的處理語句。

使用EXECUTE IMMEDIATE

執行SQL語句和PL/SQL語句塊

如下程式碼動態地建立了一個表,並向表中插入一條資料:

DECLARE
    sql_statement VARCHAR2(100);
    plsql_block VARCHAR2(500);
BEGIN
    sql_statement := 'CREATE TABLE ddl_demo(in NUMBER, amt NUMBER)';
    EXECUTE IMMEDIATE sql_statement;
    sql_statement := 'INSERT INTO ddl_demo VALUES(1, 100)';
    EXECUTE IMMEDIATE sql_statement;

    plsql_block := 
        'DECLARE
            i INTEGER := 10;
            FOR j IN 1.. i LOOP
                INSERT INTO ddl_demo VALUES(j, j * 100);
            END LOOP;
        END;'
    EXECUTE IMMEDIATE plsql_block;

要注意,使用EXECUTE IMMEDIATE執行一個SQL語句時,不要在語句後面放分號,只有在執行PL/SQL語句塊時才需要新增分號。

使用繫結變數

程式碼如下 :

DECLARE
    v_loc VARCHAR2(20) := '南京';
    v_deptno := NUMBER(2) := 30;
    sql_stmt VARCHAR2(100);
BEGIN
    sql_stmt := 'UPDATE dept SET loc = :1 WHERE deptno = :2';
    EXECUTE IMMEDIATE sql_stmt USING v_loc, v_deptno;
END;

在SQL語句中使用繫結變數時,僅能對用於資料值的表示式進行替換,比如靜態文字、變數或複雜表示式,而不能對方案元素使用繫結 變數,比如將表名和列名作繫結表示式,或者是對整個SQL語句塊使用繫結表示式,比如一個WHERE子句。如果要動態定義方案元素,需要使用字串拼接的方式對字串進行拼接。

使用RETURNING INTO子句

RETURNING INTO只能處理單行的DML語句,如果DML語句作用在多行上,則必須要使用BULK子句。

DECLARE
   v_empno NUMBER(4) := 7369;
   v_percent NUMBER(4, 2) := 0.12;
   v_salary  NUMBER(10, 2);
   sql_stmt  VARCHAR2(500);
BEGIN
    sql_stmt := 'UPDATE emp SET sal = sal * (1 + :percent) '
             || ' WHERE empno = :empno RETURNING al INTO :salary';
    EXECUTE IMMEDIATE sql_stmt USING v_percent, v_empno RETURNING INTO v_salary;
    DBMS_OUTPUT.put_line('調整後的工資為:' || v_salary);
END;

執行單行查詢

當使用動態SQL語句執行單行查詢時,可以使用EXECUTE IMMEDIATE的INTO子句將查詢的額結果欄位寫到一個或多個繫結變數或記錄型別的繫結變數中。

DECLARE
    sql_stmt  VARCHAR2(500);
    v_deptno NUMBER(4) := 20;
    v_empno NUMBER(4) := 7369;
    v_dname VARCHAR2(20);
    v_loc VARCHAR2(20);
    emp_row emp%ROWTYPE;
BEGIN
    sql_stmt := 'SELECT dname, loc FROM dept WHERE deptno = :deptno';
    EXECUTE IMMEDIATE sql_stmt INTO v_dname, v_loc USING v_deptno;

    sql_stmt := 'SELECT * FROM emp WHERE empno = :empno';
    EXECUTE IMMEDIATE sql_stmt INTO emp_row USING v_empno;

多行查詢語句

在使用靜態SQL處理多行查詢時,需要使用遊標來遍歷迴圈,動態SQL要查詢多行的話,也需要類似的處理。

看程式碼:

DECLARE
   TYPE emp_cur_type IS REF CURSOR;          --定義遊標型別    
   emp_cur emp_cur_type;                     --定義遊標變數
   v_deptno NUMBER(4) := '&deptno';          --定義部門編號繫結變數
   v_empno NUMBER(4);
   v_ename VARCHAR2(25);
BEGIN
    OPEN emp_cur FOR                          --開啟動態遊標
       'SELECT empno, ename FROM emp '||
       'WHERE deptno = :1'
    USING v_deptno;
    LOOP
        FETCH emp_cur INTO v_empno, v_ename;  --迴圈提取遊標資料
        EXIT WHEN emp_cur%NOTFOUND;           --沒有資料時退出迴圈
    END LOOP;
    CLOSE emp_cur;                            --關閉遊標變數     
EXCEPTION
    WHEN OTHERS THEN 
        IF emp_cur%FOUND THEN
            CLOSE emp_cur;
        END IF;
END;

使用批量繫結

批量EXECUTE IMMEDIATE

EXECUTE IMMEDIATE使用BULK子句來提供批量繫結的能力,程式碼如下:

DECLARE
   TYPE ename_table_type IS TABLE OF VARCHAR2(20) INDEX BY BINARY_INTEGER;
   TYPE empno_table_type IS TABLE OF NUMBER(24) INDEX BY BINARY_INTEGER; 
   ename_tab ename_table_type;              --定義儲存多行返回值的索引表
   empno_tab empno_table_type;  
   v_deptno NUMBER(4) := '&deptno';          --定義部門編號繫結變數
   sql_stmt VARCHAR2(500);
BEGIN
   --定義多行查詢的SQL語句
   sql_stmt:='SELECT empno, ename FROM emp '||'WHERE deptno = :1';
   EXECUTE IMMEDIATE sql_stmt 
   BULK COLLECT INTO empno_tab,ename_tab               --批量插入到索引表
   USING v_deptno;   
   FOR i IN 1..ename_tab.COUNT LOOP                    --輸出返回的結果值 
      DBMS_OUTPUT.put_line('員工編號'||empno_tab(i)
                                         ||'員工名稱:'||ename_tab(i));
   END LOOP;          
END;

批量OPEN FOR FETCH

OPEN FOR FETCH同樣提供了BULK子句來進行處理,程式碼如下:

DECLARE
   TYPE ename_table_type IS TABLE OF VARCHAR2(20) INDEX BY BINARY_INTEGER;
   TYPE empno_table_type IS TABLE OF NUMBER(24) INDEX BY BINARY_INTEGER;
   TYPE emp_cur_type IS REF CURSOR;         --定義遊標型別    
   ename_tab ename_table_type;              --定義儲存多行返回值的索引表
   empno_tab empno_table_type;  
   emp_cur emp_cur_type;                    --定義遊標變數
   v_deptno NUMBER(4) := '&deptno';         --定義部門編號繫結變數
BEGIN
   OPEN emp_cur FOR                         --開啟動態遊標
      'SELECT empno, ename FROM emp '||
      'WHERE deptno = :1'
   USING v_deptno;
   FETCH emp_cur BULK COLLECT INTO empno_tab, ename_tab; --批量提取遊標資料  
   CLOSE emp_cur;                                        --關閉遊標變數
   FOR i IN 1..ename_tab.COUNT LOOP                      --輸出返回的結果值 
      DBMS_OUTPUT.put_line('員工編號'||empno_tab(i)
                                         ||'員工名稱:'||ename_tab(i));
   END LOOP;       
END;

批量FORALL

之前的批量技術都是用於提取資料,而FORALL允許在EXECUTE IMMEDIATE中批量繫結輸入引數,程式碼如下:

DECLARE
   --定義索引表型別,用來儲存從DML語句中返回的結果
   TYPE ename_table_type IS TABLE OF VARCHAR2(25) INDEX BY BINARY_INTEGER;
   TYPE sal_table_type IS TABLE OF NUMBER(10,2) INDEX BY BINARY_INTEGER;   
   TYPE empno_table_type IS TABLE OF NUMBER(4);         --定義巢狀表型別,用於批量輸入員工編號  
   ename_tab ename_table_type;
   sal_tab sal_table_type;
   empno_tab empno_table_type;
   v_deptno NUMBER(4) :=20;                             --定義部門繫結變數
   v_percent NUMBER(4,2) := 0.12;                       --定義加薪比率繫結變數
   sql_stmt  VARCHAR2(500);                             --儲存SQL語句的變數
BEGIN
   empno_tab:=empno_table_type(7369,7499,7521,7566);    --初始化巢狀表
     --定義更新emp表的sal欄位值的動態SQL語句
   sql_stmt:='UPDATE emp SET sal=sal*(1+:percent) '
             ||' WHERE empno=:empno RETURNING ename,sal INTO :ename,:salary';
   FORALL i IN 1..empno_tab.COUNT                        --使用FORALL語句批量輸入引數
      EXECUTE IMMEDIATE sql_stmt USING v_percent, empno_tab(i)  --這裡使用來自巢狀表的引數
      RETURNING BULK COLLECT INTO ename_tab,sal_tab;   --使用RETURNING BULK COLLECT INTO子句獲取返回值
   FOR i IN 1..ename_tab.COUNT LOOP                    --輸出返回的結果值 
      DBMS_OUTPUT.put_line('員工'||ename_tab(i)||'調薪後的薪資:'||sal_tab(i));
   END LOOP;
END;

動態SQL的使用建議

用繫結變數改善效能

看如下程式碼:

EXECUTE IMMEDIATE 'DELETE FROM emp WHERE empno = ' || TO_CAHR(emp_id);
EXECUTE IMMEDIATE 'DELETE FROM emp WHERE empno = :num' USING emp_id;

這兩行的愛嗎效果是一樣的,但是建議優先考慮繫結變數而不是字串連線,原因如下:
- 繫結比連線具有更高的效能:由於使用繫結變數,不會每次都改變SQL語句,因此可以使用SGA中快取的預備遊標來快速處理SQL語句。
- 繫結變數更容易編寫和維護:使用繫結變數不用擔心資料型別轉換的問題,本地動態SQL引擎可以處理所有關於轉換相關的額問題,而對於拼接字串來說,必須要經常使用TO_CHAR,TO_DATE等函式處理資料型別轉換。
- 避免隱式型別轉換:連線SQL可能會導致資料庫隱式轉換,有可能會導致隱式轉換為不想要的結果。
- 繫結避免程式碼注入:使用繫結變數可以避免SQL注入攻擊,而連線字串有可能會導致這種危險的情形。

使用呼叫者許可權

預設情況下,呼叫動態SQL子程式的使用者是使用建立者的許可權來執行動態SQL,可以通過在子程式上使用AUTHID子句使得子程式使用呼叫者許可權來執行,這樣就不會繫結在一個特定的schema物件上,程式碼如下:

CREATE OR REPLACE PROCEDURE drop_obj (kind IN VARCHAR2, NAME IN VARCHAR2)
AUTHID CURRENT_USER       --定義呼叫者許可權
AS
BEGIN
   EXECUTE IMMEDIATE 'DROP ' || kind || ' ' || NAME;
EXCEPTION
WHEN OTHERS THEN
   RAISE;   
END;

傳遞NULL引數

如果要為動態SQL傳遞NULL值,直接寫USING NULL會導致錯誤,因為USING語句不接受NULL作為傳遞的引數。為了解決這個問題,可以定義一個未賦值的變數,該變數在未賦值時自動為NULL值,程式碼如下:

DECLARE
   v_null   CHAR (1);                      --在執行時該變數自動被設定為NULL值
BEGIN
   EXECUTE IMMEDIATE 'UPDATE emp SET comm = :x' USING v_null;  --傳入NULL值
END;

動態SQL異常處理

在動態拼接一個SQL時,因為拼寫錯誤或空格問題都會導致語句執行失敗,此時Oracle會丟擲錯誤提示,但通常這種錯誤資訊很不全面,初遇應用程式健壯性的考慮,提供以下幾個建議:
- 總是在呼叫EXECUTE IMMEDIATE和OPEN FOR語句的地方包含異常處理塊。
- 在每一個異常處理塊中記錄錯誤訊息和執行的SQL語句,以便發現錯誤。
- 可以使用DBMS_OUTPUT包新增一個追蹤機制以便能更好地發現錯誤。

程式碼如下:

CREATE OR REPLACE PROCEDURE ddl_execution (ddl_string IN VARCHAR2)
   AUTHID CURRENT_USER IS            --使用呼叫者許可權
BEGIN
   EXECUTE IMMEDIATE ddl_string;     --執行動態SQL語句
EXCEPTION
   WHEN OTHERS                       --捕捉錯誤  
   THEN
      DBMS_OUTPUT.PUT_LINE (      --顯示錯誤訊息
         '動態SQL語句錯誤:' || DBMS_UTILITY.FORMAT_ERROR_STACK);
      DBMS_OUTPUT.PUT_LINE (      --顯示當前執行的SQL語句
         '執行的SQL語句為: "' || ddl_string || '"');
      RAISE;
END ddl_execution;