1. 程式人生 > 其它 >PL/SQL 程式設計(二)遊標、儲存過程、函式

PL/SQL 程式設計(二)遊標、儲存過程、函式

遊標--資料的快取區

遊標:類似集合,可以讓使用者像運算元組一樣操作查詢出來的資料集,實質上,它提供了一種從集合性質的結果中提取單條記錄的手段。

可以將遊標形象的看成一個變動的游標,他實質上是一個指標,在一段Oracle存放資料查詢結果集或者資料操作結果集的記憶體中,這個指標可以指向結果集任何一條記錄。

遊標分靜態遊標和REF遊標兩類,靜態遊標包含顯式遊標和隱式遊標。

顯式遊標:

在使用之前必須有明確的遊標宣告和定義,這樣的遊標定義會關聯資料查詢語句,通常會返回一行或多行。開啟遊標後,使用者可以利用遊標的位置對結果集進行檢索,使之返回單一的行記錄,使用者可以操作此記錄。

顯式遊標需要使用者自己寫程式碼完成,一切由使用者控制。

顯式遊標處理需四個 PL/SQL步驟:

l 定義/宣告遊標:就是定義一個遊標名,以及與其相對應的SELECT 語句。

遊標引數只能為輸入引數。

在指定資料型別時,不能使用長度約束。如NUMBER(4),CHAR(10) 等都是錯誤的。

l 開啟遊標:就是執行遊標所對應的SELECT 語句,將其查詢結果放入工作區,並且指標指向工作區的首部,標識遊標結果集合。如果遊標查詢語句中帶有FOR UPDATE選項,OPEN 語句還將鎖定資料庫表中游標結果集合對應的資料行。

在向遊標傳遞引數時,可以使用與函式引數相同的傳值方法,即位置表示法和名稱表示法。PL/SQL 程式不能用OPEN 語句重複開啟一個遊標。

l 提取遊標資料:就是檢索結果集合中的資料行,放入指定的輸出變數中。 

執行FETCH語句時,每次返回一個數據行,然後自動將遊標移動指向下一個資料行。當檢索到最後一行資料時,如果再次執行FETCH語句,將操作失敗,並將遊標屬性%NOTFOUND置為TRUE。所以每次執行完FETCH語句後,檢查遊標屬性%NOTFOUND就可以判斷FETCH語句是否執行成功並返回一個數據行,以便確定是否給對應的變數賦了值。 

l 對該記錄進行處理;

l 繼續處理,直到活動集合中沒有記錄;

l 關閉遊標:當提取和處理完遊標結果集合資料後,應及時關閉遊標,以釋放該遊標所佔用的系統資源,並使該遊標的工作區變成無效,不能再使用FETCH 語句取其中資料。關閉後的遊標可以使用OPEN 語句重新開啟。

注意:定義的遊標不能有INTO 子句。

--查詢前10名員工的資訊。
 
DECLARE
   CURSOR c_cursor 
   IS SELECT first_name || last_name, Salary 
   FROM EMPLOYEES 
   WHERE rownum<11;   
   v_ename  EMPLOYEES.first_name%TYPE;
   v_sal    EMPLOYEES.Salary%TYPE;   
BEGIN
  OPEN c_cursor;
  FETCH c_cursor INTO v_ename, v_sal;
  WHILE c_cursor%FOUND LOOP
     DBMS_OUTPUT.PUT_LINE(v_ename||'---'||to_char(v_sal) );
     FETCH c_cursor INTO v_ename, v_sal;
  END LOOP;
  CLOSE c_cursor;
END;
--遊標引數的傳遞方法。
 
DECLARE
  DeptRec    DEPARTMENTS%ROWTYPE;
  Dept_name  DEPARTMENTS.DEPARTMENT_NAME%TYPE;
  Dept_loc   DEPARTMENTS.LOCATION_ID%TYPE;
  CURSOR c1 IS 
  SELECT DEPARTMENT_NAME, LOCATION_ID FROM DEPARTMENTS 
  WHERE DEPARTMENT_ID <= 30;
  
  CURSOR c2(dept_no NUMBER DEFAULT 10) IS
    SELECT DEPARTMENT_NAME, LOCATION_ID FROM DEPARTMENTS 
    WHERE DEPARTMENT_ID <= dept_no;
  CURSOR c3(dept_no NUMBER DEFAULT 10) IS 
    SELECT * FROM DEPARTMENTS 
    WHERE DEPARTMENTS.DEPARTMENT_ID <=dept_no;
BEGIN
  OPEN c1;
  LOOP
    FETCH c1 INTO dept_name, dept_loc;
    EXIT WHEN c1%NOTFOUND;
        DBMS_OUTPUT.PUT_LINE(dept_name||'---'||dept_loc);
    END LOOP;
    CLOSE c1;

    OPEN c2;
    LOOP
        FETCH c2 INTO dept_name, dept_loc;
        EXIT WHEN c2%NOTFOUND;
        DBMS_OUTPUT.PUT_LINE(dept_name||'---'||dept_loc);
    END LOOP;
    CLOSE c2;

    OPEN c3(dept_no =>20);
    LOOP
        FETCH c3 INTO deptrec;
        EXIT WHEN c3%NOTFOUND;
        DBMS_OUTPUT.PUT_LINE(deptrec.DEPARTMENT_ID||'---'||deptrec.DEPARTMENT_NAME||'---'||deptrec.LOCATION_ID);
    END LOOP;
    CLOSE c3;
END;
--給工資低於1200 的員工增加工資50。
 
DECLARE
   v_empno  EMPLOYEES.EMPLOYEE_ID%TYPE;
   v_sal      EMPLOYEES.Salary%TYPE;
   CURSOR c_cursor IS SELECT EMPLOYEE_ID, Salary FROM EMPLOYEES; 
BEGIN
   OPEN c_cursor;
   LOOP
      FETCH c_cursor INTO v_empno, v_sal;
      EXIT WHEN c_cursor%NOTFOUND; 
      IF v_sal<=1200 THEN
            UPDATE EMPLOYEES SET Salary=Salary+50 WHERE EMPLOYEE_ID=v_empno;
            DBMS_OUTPUT.PUT_LINE('編碼為'||v_empno||'工資已更新!');
      END IF;
   DBMS_OUTPUT.PUT_LINE('記錄數:'|| c_cursor %ROWCOUNT);
   END LOOP;
   CLOSE c_cursor;
END; 
--沒有引數且沒有返回值的遊標。
 
DECLARE
   v_f_name employees.first_name%TYPE;
   v_j_id   employees.job_id%TYPE;
   CURSOR c1       --宣告遊標,沒有引數沒有返回值
   IS
      SELECT first_name, job_id FROM employees 
      WHERE department_id = 20;
BEGIN
   OPEN c1;        --開啟遊標
   LOOP
      FETCH c1 INTO v_f_name, v_j_id;    --提取遊標
      IF c1%FOUND THEN
         DBMS_OUTPUT.PUT_LINE(v_f_name||'的崗位是'||v_j_id);
      ELSE
         DBMS_OUTPUT.PUT_LINE('已經處理完結果集了');
         EXIT;
      END IF;
   END LOOP;
   CLOSE c1;   --關閉遊標
END;
--有引數且沒有返回值的遊標。
 
DECLARE
   v_f_name employees.first_name%TYPE;
   v_h_date employees.hire_date%TYPE;
   CURSOR c2(dept_id NUMBER, j_id VARCHAR2) --宣告遊標,有引數沒有返回值
   IS
      SELECT first_name, hire_date FROM employees
      WHERE department_id = dept_id AND job_id = j_id;
BEGIN
   OPEN c2(90, 'AD_VP');  --開啟遊標,傳遞引數值
   LOOP
      FETCH c2 INTO v_f_name, v_h_date;    --提取遊標
      IF c2%FOUND THEN
         DBMS_OUTPUT.PUT_LINE(v_f_name||'的僱傭日期是'||v_h_date);
      ELSE
         DBMS_OUTPUT.PUT_LINE('已經處理完結果集了');
         EXIT;
      END IF;
   END LOOP;
   CLOSE c2;   --關閉遊標
END;
--有引數且有返回值的遊標。
 
DECLARE
   TYPE emp_record_type IS RECORD(
        f_name   employees.first_name%TYPE,
        h_date   employees.hire_date%TYPE);
   v_emp_record EMP_RECORD_TYPE;

   CURSOR c3(dept_id NUMBER, j_id VARCHAR2) --宣告遊標,有引數有返回值
          RETURN EMP_RECORD_TYPE
   IS
      SELECT first_name, hire_date FROM employees
      WHERE department_id = dept_id AND job_id = j_id;
BEGIN
   OPEN c3(j_id => 'AD_VP', dept_id => 90);  --開啟遊標,傳遞引數值
   LOOP
      FETCH c3 INTO v_emp_record;    --提取遊標
      IF c3%FOUND THEN
         DBMS_OUTPUT.PUT_LINE(v_emp_record.f_name||'的僱傭日期是'
                            ||v_emp_record.h_date);
      ELSE
         DBMS_OUTPUT.PUT_LINE('已經處理完結果集了');
         EXIT;
      END IF;
   END LOOP;
   CLOSE c3;   --關閉遊標
END;
--基於遊標定義記錄變數。
 
DECLARE
   CURSOR c4(dept_id NUMBER, j_id VARCHAR2) --宣告遊標,有引數沒有返回值
   IS
      SELECT first_name f_name, hire_date FROM employees
      WHERE department_id = dept_id AND job_id = j_id;
    --基於遊標定義記錄變數,比宣告記錄型別變數要方便,不容易出錯
    v_emp_record c4%ROWTYPE;
BEGIN
   OPEN c4(90, 'AD_VP');  --開啟遊標,傳遞引數值
   LOOP
      FETCH c4 INTO v_emp_record;    --提取遊標
      IF c4%FOUND THEN
         DBMS_OUTPUT.PUT_LINE(v_emp_record.f_name||'的僱傭日期是'
                            ||v_emp_record.hire_date);
      ELSE
         DBMS_OUTPUT.PUT_LINE('已經處理完結果集了');
         EXIT;
      END IF;
   END LOOP;
   CLOSE c4;   --關閉遊標
END;

隱式遊標:

被plsql自動管理,也被稱為sql遊標,

使用者無法控制,但能得到他的屬性資訊。

對於非查詢語句,如修改、刪除操作,由ORACLE 系統自動地為這些操作設定遊標並建立其工作區,這些由系統隱含建立的遊標稱為隱式遊標,隱式遊標的名字為SQL,這是由ORACLE 系統定義的。對於隱式遊標的操作,如定義、開啟、取值及關閉操作,都由ORACLE 系統自動地完成,無需使用者進行處理。使用者只能通過隱式遊標的相關屬性,來完成相應的操作。在隱式遊標的工作區中,所存放的資料是與使用者自定義的顯示遊標無關的、最新處理的一條SQL 語句所包含的資料。

格式呼叫為: SQL%

注:INSERT, UPDATE, DELETE, SELECT 語句中不必明確定義遊標。

--刪除EMPLOYEES表中某部門的所有員工,如果該部門中已沒有員工,則在DEPARTMENT表中刪除該部門。
 
DECLARE
    V_deptno department_id%TYPE :=&p_deptno;
BEGIN
    DELETE FROM employees WHERE department_id=v_deptno;
    IF SQL%NOTFOUND THEN
        DELETE FROM departments WHERE department_id=v_deptno;
    END IF;
END;
--通過隱式遊標SQL的%ROWCOUNT屬性來了解修改了多少行。
DECLARE
   v_rows NUMBER;
BEGIN
--更新資料
   UPDATE employees SET salary = 30000
   WHERE department_id = 90 AND job_id = 'AD_VP';
--獲取預設遊標的屬性值
   v_rows := SQL%ROWCOUNT;
   DBMS_OUTPUT.PUT_LINE('更新了'||v_rows||'個僱員的工資');
--回退更新,以便使資料庫的資料保持原樣
   ROLLBACK;
END;

儲存過程

儲存過程就是一段儲存在資料庫中執行某種功能的程式。簡單來時是儲存在資料庫伺服器中的封裝了一段或多段sql語句的plsql程式碼塊。儲存過程可以在程式語言中呼叫,如Java等。

儲存過程的優點:

簡化複雜的操作,封裝。

增加資料獨立性,利用儲存過程可以把資料庫基礎資料和程式或使用者隔離開來。

提高安全性。

提高效能。

有參儲存過程:

儲存過程允許帶有引數,過程有輸入,輸出,輸入輸出三種引數。

輸入 :in 預設,可省略

輸出 :out

--輸出引數
CREATE OR REPLACE PROCEDURE HANQI2(SCLA IN NUMBER, vari OUT number) AS

BEGIN
  UPDATE STUDENT S SET S.SSEX = '女' WHERE S.CLASS = SCLA;
  SELECT COUNT(*) INTO vari FROM student s WHERE s.class=scla;
  DBMS_OUTPUT.PUT_LINE('記錄已經修改 !');
END;
--使用者連線登記記錄; 
CREATE TABLE logtable (userid VARCHAR2(10), logdate date);

CREATE OR REPLACE PROCEDURE logexecution 
IS
BEGIN
INSERT INTO logtable (userid, logdate) VALUES (USER, SYSDATE);
END;
--刪除指定員工記錄; 
CREATE OR REPLACE
PROCEDURE DelEmp
(v_empno IN employees.employee_id%TYPE) 
AS
No_result EXCEPTION;
BEGIN
   DELETE FROM employees WHERE employee_id = v_empno;
   IF SQL%NOTFOUND THEN
      RAISE no_result;
   END IF;
   DBMS_OUTPUT.PUT_LINE('編碼為'||v_empno||'的員工已被刪除!');
EXCEPTION
   WHEN no_result THEN 
      DBMS_OUTPUT.PUT_LINE('溫馨提示:你需要的資料不存在!');
   WHEN OTHERS THEN
      DBMS_OUTPUT.PUT_LINE(SQLCODE||'---'||SQLERRM);
END DelEmp;
--插入員工記錄: 
CREATE OR REPLACE
PROCEDURE InsertEmp(
   v_empno     in employees.employee_id%TYPE,
   v_firstname in employees.first_name%TYPE,
   v_lastname  in employees.last_name%TYPE,
   v_deptno    in employees.department_id%TYPE
   ) 
AS
   empno_remaining EXCEPTION;
   PRAGMA EXCEPTION_INIT(empno_remaining, -1);
   /* -1 是違反唯一約束條件的錯誤程式碼 */
BEGIN
   INSERT INTO EMPLOYEES(EMPLOYEE_ID, FIRST_NAME, LAST_NAME, HIRE_DATE,DEPARTMENT_ID)
   VALUES(v_empno, v_firstname,v_lastname, sysdate, v_deptno);
   DBMS_OUTPUT.PUT_LINE('溫馨提示:插入資料記錄成功!');
EXCEPTION
   WHEN empno_remaining THEN 
      DBMS_OUTPUT.PUT_LINE('溫馨提示:違反資料完整性約束!');
   WHEN OTHERS THEN
      DBMS_OUTPUT.PUT_LINE(SQLCODE||'---'||SQLERRM);
END InsertEmp;
--使用儲存過程向departments表中插入資料。 
CREATE OR REPLACE
PROCEDURE insert_dept
  (v_dept_id IN departments.department_id%TYPE,
   v_dept_name IN departments.department_name%TYPE,
   v_mgr_id IN departments.manager_id%TYPE,
   v_loc_id IN departments.location_id%TYPE)
IS
   ept_null_error EXCEPTION;
   PRAGMA EXCEPTION_INIT(ept_null_error, -1400);
   ept_no_loc_id EXCEPTION;
   PRAGMA EXCEPTION_INIT(ept_no_loc_id, -2291);
BEGIN
   INSERT INTO departments
   (department_id, department_name, manager_id, location_id)
   VALUES
   (v_dept_id, v_dept_name, v_mgr_id, v_loc_id);
   DBMS_OUTPUT.PUT_LINE('插入部門'||v_dept_id||'成功');
EXCEPTION
   WHEN DUP_VAL_ON_INDEX THEN
      RAISE_APPLICATION_ERROR(-20000, '部門編碼不能重複');
   WHEN ept_null_error THEN
      RAISE_APPLICATION_ERROR(-20001, '部門編碼、部門名稱不能為空');
   WHEN ept_no_loc_id THEN
      RAISE_APPLICATION_ERROR(-20002, '沒有該地點');
END insert_dept;
--呼叫例項一:
DECLARE
   ept_20000 EXCEPTION;
   PRAGMA EXCEPTION_INIT(ept_20000, -20000);
   ept_20001 EXCEPTION;
   PRAGMA EXCEPTION_INIT(ept_20001, -20001);
   ept_20002 EXCEPTION;
   PRAGMA EXCEPTION_INIT(ept_20002, -20002);
BEGIN
   insert_dept(300, '部門300', 100, 2400);
   insert_dept(310, NULL, 100, 2400);
   insert_dept(310, '部門310', 100, 900);
EXCEPTION
   WHEN ept_20000 THEN
      DBMS_OUTPUT.PUT_LINE('ept_20000部門編碼不能重複');
   WHEN ept_20001 THEN
      DBMS_OUTPUT.PUT_LINE('ept_20001部門編碼、部門名稱不能為空');
   WHEN ept_20002 THEN
      DBMS_OUTPUT.PUT_LINE('ept_20002沒有該地點');
   WHEN OTHERS THEN
      DBMS_OUTPUT.PUT_LINE('others出現了其他異常錯誤');
END;

--呼叫例項二:
DECLARE
   ept_20000 EXCEPTION;
   PRAGMA EXCEPTION_INIT(ept_20000, -20000);
   ept_20001 EXCEPTION;
   PRAGMA EXCEPTION_INIT(ept_20001, -20001);
   ept_20002 EXCEPTION;
   PRAGMA EXCEPTION_INIT(ept_20002, -20002);
BEGIN
   insert_dept(v_dept_name => '部門310', v_dept_id => 310, 
               v_mgr_id => 100, v_loc_id => 2400);
   insert_dept(320, '部門320', v_mgr_id => 100, v_loc_id => 900);
EXCEPTION
   WHEN ept_20000 THEN
      DBMS_OUTPUT.PUT_LINE('ept_20000部門編碼不能重複');
   WHEN ept_20001 THEN
      DBMS_OUTPUT.PUT_LINE('ept_20001部門編碼、部門名稱不能為空');
   WHEN ept_20002 THEN
      DBMS_OUTPUT.PUT_LINE('ept_20002沒有該地點');
   WHEN OTHERS THEN
      DBMS_OUTPUT.PUT_LINE('others出現了其他異常錯誤');
END;

函式

函式主要組成部分:

輸入部分。輸入引數,呼叫函式時給這些引數賦值。

邏輯計算部分。函式內部將完成對各種資料專案的計算。

輸出部分。函式必須有返回值。

--輸入引數,計算和
CREATE OR REPLACE FUNCTION CAL_ADD(A IN NUMBER, B IN NUMBER) RETURN NUMBER;
AS
C NUMBER;
BEGIN
C := A + B; 
RETURN C;
END;
--獲取某部門的工資總和
CREATE OR REPLACE
FUNCTION get_salary(
  Dept_no NUMBER,
  Emp_count OUT NUMBER)
  RETURN NUMBER 
IS
  V_sum NUMBER;
BEGIN
  SELECT SUM(SALARY), count(*) INTO V_sum, emp_count
    FROM EMPLOYEES WHERE DEPARTMENT_ID=dept_no;
  RETURN v_sum;
EXCEPTION
   WHEN NO_DATA_FOUND THEN 
      DBMS_OUTPUT.PUT_LINE('你需要的資料不存在!');
   WHEN OTHERS THEN 
      DBMS_OUTPUT.PUT_LINE(SQLCODE||'---'||SQLERRM);
END get_salary;

用程式在呼叫函式時,可以使用以下三種方法向函式傳遞引數:

第一種引數傳遞格式:位置表示法。

即在呼叫時按形參的排列順序,依次寫出實參的名稱,而將形參與實參關聯起來進行傳遞。用這種方法進行呼叫,形參與實參的名稱是相互獨立,沒有關係,強調次序才是重要的。

--計算某部門的工資總和: 
DECLARE
  V_num NUMBER;
  V_sum NUMBER;
BEGIN
  V_sum :=get_salary(10, v_num);
  DBMS_OUTPUT.PUT_LINE('部門號為:10的工資總和:'||v_sum||',人數為:'||v_num);
END;

第二種引數傳遞格式:名稱表示法。

即在呼叫時按形參的名稱與實參的名稱,寫出實參對應的形參,而將形參與實參關聯起來進行傳遞。這種方法,形參與實參的名稱是相互獨立的,沒有關係,名稱的對應關係才是最重要的,次序並不重要。

--計算某部門的工資總和: 
DECLARE
  V_num NUMBER;
    V_sum NUMBER;
BEGIN
    V_sum :=get_salary(emp_count => v_num, dept_no => 10);
    DBMS_OUTPUT.PUT_LINE('部門號為:10的工資總和:'||v_sum||',人數為:'||v_num);
END;

第三種引數傳遞格式:組合傳遞。

即在呼叫一個函式時,同時使用位置表示法和名稱表示法為函式傳遞引數。採用這種引數傳遞方法時,使用位置表示法所傳遞的引數必須放在名稱表示法所傳遞的引數前面。也就是說,無論函式具有多少個引數,只要其中有一個引數使用名稱表示法,其後所有的引數都必須使用名稱表示法。

CREATE OR REPLACE FUNCTION demo_fun(
  Name VARCHAR2,--注意VARCHAR2不能給精度,如:VARCHAR2(10),其它類似
  Age INTEGER,
  Sex VARCHAR2)
  RETURN VARCHAR2 
AS
  V_var VARCHAR2(32);
BEGIN
  V_var := name||':'||TO_CHAR(age)||'歲.'||sex;
  RETURN v_var;
END;
DECLARE 
  Var VARCHAR(32);
BEGIN
  Var := demo_fun('user1', 30, sex => '男');
  DBMS_OUTPUT.PUT_LINE(var);

  Var := demo_fun('user2', age => 40, sex => '男');
  DBMS_OUTPUT.PUT_LINE(var);

  Var := demo_fun('user3', sex => '女', age => 20);
  DBMS_OUTPUT.PUT_LINE(var);
END;

無論採用哪一種引數傳遞方法,實際引數和形式引數之間的資料傳遞只有兩種方法:傳址法和傳值法。

傳址法:指在呼叫函式時,將實際引數的地址指標傳遞給形式引數,使形式引數和實際引數指向記憶體中的同一區域,從而實現引數資料的傳遞。這種方法又稱作參照法,即形式引數參照實際引數資料。輸入引數均採用傳址法傳遞資料。

傳值法:指將實際引數的資料拷貝到形式引數,而不是傳遞實際引數的地址。預設時,輸出引數和輸入/輸出引數均採用傳值法。在函式呼叫時,ORACLE將實際引數資料拷貝到輸入/輸出引數,而當函式正常執行退出時,又將輸出形式引數和輸入/輸出形式引數資料拷貝到實際引數變數中。

注意:在CREATE OR REPLACE FUNCTION 語句中宣告函式引數時可以使用DEFAULT關鍵字為輸入引數指定預設值。

CREATE OR REPLACE FUNCTION demo_fun(
  Name VARCHAR2,
  Age INTEGER,
  Sex VARCHAR2 DEFAULT '男')
  RETURN VARCHAR2 
AS
  V_var VARCHAR2(32);
BEGIN
  V_var := name||':'||TO_CHAR(age)||'歲.'||sex;
  RETURN v_var;
END;

具有預設值的函式建立後,在函式呼叫時,如果沒有為具有預設值的引數提供實際引數值,函式將使用該引數的預設值。但當呼叫者為預設引數提供實際引數時,函式將使用實際引數值。在建立函式時,只能為輸入引數設定預設值,而不能為輸入/輸出引數設定預設值。

DECLARE 
 var VARCHAR(32);
BEGIN
 Var := demo_fun('user1', 30);
 DBMS_OUTPUT.PUT_LINE(var);
 Var := demo_fun('user2', age => 40);
 DBMS_OUTPUT.PUT_LINE(var);
 Var := demo_fun('user3', sex => '女', age => 20);
 DBMS_OUTPUT.PUT_LINE(var);
END;