2016年2月28日 星期日

Trigger六(調用子程序) (PL/SQL 二十三)

尤於trigger不能超過32KB,所以要寫很多的時候,可以提出來成為procedure或function

CREATE OR REPLACE PROCEDURE xxx IS 
BEGIN 
    DBMS_OUTPUT.PUT_LINE('procedure');
END;
    
    
    
CREATE OR REPLACE FUNCTION ooo RETURN VARCHAR2 IS
BEGIN
    DBMS_OUTPUT.PUT_LINE('function');
    RETURN 'success';
END;
    
    
    
CREATE OR REPLACE TRIGGER test_ddl
    BEFORE DDL ON SCHEMA ENABLE
BEGIN
    xxx;
    IF ooo() = 'success' THEN
        RAISE_APPLICATION_ERROR(-20000, '不能使用DDL');
    END IF;
END test_ddl;

※這時隨便create(假設create sequence),就OK了

※刪除時,必須先刪除trigger,如果先刪除procedure或function會出錯

安裝Oracle12c

Oracle官網下載安裝檔,這裡下載的是12.1.0.2.0標準版的,選接受下載後有File1和File2



1.下載完後有兩個zip檔,按右鍵解壓,兩個都要解,解完後只有一個database資料夾
※有些版本會有兩個資料夾, 這時就要將兩個資料夾合成一個資料夾,這個版本只要將2資料夾database\stage\Components下的檔案複製到1資料夾裡就行了,否則安裝時會找不到 WFMLRSVCApp.ear 這支檔案,且如果已經安裝失敗了,必需將 app 資料夾刪除,否則再次安裝時會有找不到路徑的錯誤


2.執行打上「services.msc」目前還沒有相關的Oracle資料庫訊息


3.執行打上「sysdm.cpl」,進階-->環境變數裡的path目前如下,也是沒有Oracle的訊息
%SystemRoot%\system32;
%SystemRoot%;
%SystemRoot%\System32\Wbem;
%SYSTEMROOT%\System32\WindowsPowerShell\v1.0\


4.打開database裡的setup.exe


5.大部分的人都不想要收到安全更新,所以不勾,按下一步只會出現警告訊息,不鳥它,左下角的說明每一頁按下去都有目前的說明


6.第一次選這些就可以了,如果是筆電就選桌面類別吧,如果想了解更深入的,再選進階安裝,不然一般安裝就以足夠


7.這裡輸入的使用者將來會在app下有一個資料夾,資料夾名稱就是現在輸入的


8.軟體位置記一下,而全域資料庫名稱,待會安裝完後會在services.msc看到,密碼是sys帳號的,按下左下的說明有講


9.密碼的要求如下


10.確認都沒問題就可以安裝了


11.這時的等待比較快,差不多3分鐘左右



12.這兩個防火牆會檔住,都允許吧,按下允許才是漫長的等待


13.等最久的就是這個資料庫組態輔助程式


14.終於安裝完成



15.以下的路徑先將副檔名是ora的備份起來,以備不時之須,oracleusr,是第7張圖打的


16.services.msc也出現6個和資料庫有關的功能,預設只有排程停用,最後有ORCL的字是因為第8張圖的全域資料域名稱的關係,如果那邊打什麼,這邊後面的字就是什麼


17.環境變數在最前面也會多出oracle的訊息
C:\app\oracleuser\product\12.1.0\dbhome_1\bin;
%SystemRoot%\system32;
%SystemRoot%;
%SystemRoot%\System32\Wbem;
%SYSTEMROOT%\System32\WindowsPowerShell\v1.0\


18.新建使用者
執行輸入「sqlplus /nolog」
conn sys as sysdba
輸入sys的密碼 (第8張圖設定的)
create user 新使用者 identified by 密碼;
grant connect,resource,unlimited tablespace to 新使用者container=all;
alter user 新使用者 default tablespace users;
alter user 新使用者 temporary tablespace temp;
conn 新使用者
輸入新使用者的密碼

19.打開SQL Developer,在「連線」按右鍵新建連線,一般使用者如下設定,SID是第8張圖的全域資料庫名稱,預設就有了,打完後按測試,成功即可儲存
如果是DBA,那使用者名稱就是「sys」,然後角色選SYSDBA,其他和一般使用者一樣


20.上一張圖的連線名稱會在這裡顯示,如果要修改,就對名稱按右鍵選最下面的特性

21.SELECT * FROM V$VERSION; 可查看Oracle版本

22.如果想查官方的文件看這篇


※亂碼

可以下 select userenv('language') from dual; 查看當前的設定
但在 工具->偏好設定 裡的資料庫->NLS,我只找到控制「.」左邊的設定,右邊的編碼就不知道了,所以我使用以下的方式才可以,但要記得重啟電腦
環境變數只要和查到的編碼一樣就不會亂碼了,一般台灣就是以下兩個

NLS_LANG
AMERICAN_AMERICA.AL32UTF
AMERICAN_TAIWAN.ZHT16MSWIN950



2016年2月27日 星期六

Trigger五(系統 trigger DDL、登入/登出、開啟/關閉、錯誤訊息、啟用/禁用) (PL/SQL 二十二)

官網連結


※禁止使用DDL

CREATE OR REPLACE TRIGGER test_ddl
    BEFORE DDL ON SCHEMA 
BEGIN
    RAISE_APPLICATION_ERROR(-20000, '不能使用DDL');
END test_ddl;

※執行「CREATE SEQUENCE xxx;」就會出現訊息

※以下的練習還是要用trigger,所以先將trigger刪除,「DROP TRIGGER test_ddl;」

※ON SCHEMA一般用戶就可以CREATE; 但ON DATABASE預設沒權限

官網 的 Table 9-5 Database Event Triggers 和 Table 9-6 Client Event Triggers可以參考



※系統定義的事件屬性

CREATE OR REPLACE TRIGGER test_ddl
    AFTER CREATE OR DROP OR ALTER ON DATABASE
BEGIN
    DBMS_OUTPUT.PUT_LINE('ORA_LOGIN_USER=' || ORA_LOGIN_USER);
    DBMS_OUTPUT.PUT_LINE('ORA_DICT_OBJ_TYPE=' || ORA_DICT_OBJ_TYPE);
    DBMS_OUTPUT.PUT_LINE('ORA_DICT_OBJ_OWNER=' || ORA_DICT_OBJ_OWNER);
    BMS_OUTPUT.PUT_LINE('ORA_DICT_OBJ_NAME=' || ORA_DICT_OBJ_NAME);
    BMS_OUTPUT.PUT_LINE('ORA_SYSEVENT=' || ORA_SYSEVENT);
END test_ddl;

※執行「DROP SEQUENCE xxx;」會出現以下結果:
ORA_LOGIN_USER=SYS
ORA_DICT_OBJ_TYPE=SEQUENCE
ORA_DICT_OBJ_OWNER=SYS
ORA_DICT_OBJ_NAME=XXX
ORA_SYSEVENT=DROP

ORA_DICT_OBJ_OWNER是SYS是因為我用SYS用戶進來的,才有權限,只要是ON DATABASE,預設一般用戶沒有權限

※還有很多屬性,可參考官網 的Table 9-4 System-Defined Event Attributes



※控制特定欄位不能修改和刪除

CREATE OR REPLACE trigger test_ddl
    BEFORE ALTER ON SCHEMA
DECLARE
    CURSOR xxx_emp(
        table_owner all_tab_columns.OWNER%TYPE,
        table_table_name all_tab_columns.TABLE_NAME%TYPE
    ) IS
    SELECT column_name FROM all_tab_columns
    WHERE owner = table_owner
    AND table_name = table_table_name;
BEGIN
    IF ora_dict_obj_type = 'TABLE' THEN
        FOR emp_row IN xxx_emp(ORA_DICT_OBJ_OWNER, ORA_DICT_OBJ_NAME) LOOP
    
            IF ora_is_alter_column(emp_row.column_name) THEN
                IF emp_row.column_name IN('EMPNO', 'DEPTNO') THEN
                    raise_application_error(-20000, '不能修改EMPNO和DEPTNO欄位');
                END IF;
            END IF;
    
            IF ora_is_drop_column(emp_row.column_name) THEN
                IF emp_row.column_name IN('EMPNO', 'DEPTNO') THEN
                    raise_application_error(-20001, '不能刪除EMPNO和DEPTNO欄位');
                END IF;
            END IF;
    
        END LOOP;
    END IF;
END test_ddl;

※ALTER TABLE emp MODIFY(deptno);
ALTER TABLE emp DROP COLUMN empno;



※登入/登出記錄表

DROP TABLE login_logout_log PURGE;
CREATE TABLE login_logout_log(
    id NUMBER GENERATED BY DEFAULT AS IDENTITY,
    username VARCHAR2(20),
    login_date DATE,
    logout_date DATE,
    ip VARCHAR2(30)
);
    
SELECT * FROM login_logout_log;

※因為以下都是ON DATABASE,所以都用有權限的使用者來創建



※登入/登出、開啟/關閉 Trigger

CREATE OR REPLACE TRIGGER XXX_LOGIN
    AFTER LOGON ON DATABASE
BEGIN
    INSERT INTO LOGIN_LOGOUT_LOG(username, login_date, ip)
    VALUES(ORA_LOGIN_USER, SYSDATE, ORA_CLIENT_IP_ADDRESS);
END XXX_LOGIN;
    
CREATE OR REPLACE TRIGGER XXX_LOGOUT
    BEFORE LOGOFF ON DATABASE
BEGIN
    INSERT INTO LOGIN_LOGOUT_LOG(username, logout_date, ip)
    VALUES(ORA_LOGIN_USER, SYSDATE, ORA_CLIENT_IP_ADDRESS);
END XXX_LOGOUT;

※登入/登出就會紀錄在表裡了

※開啟/關閉大同小異,只要改成AFTER STARTUP和BEFORE SHUTDOWN,然後執行SHUTDOWN IMMEDIATE和STARTUP就會記錄在表裡了,不過要注意PL/SQL Developer不支援這兩個命令,會出現「ORA-00900: invalid SQL statement」



※錯誤訊息 Trigger

CREATE OR REPLACE TRIGGER XXX_ERROR
    AFTER SERVERERROR ON DATABASE
BEGIN
    INSERT INTO ERR_LOG(dbname, err_date, err_msg)
    VALUES(ORA_DATABASE_NAME, SYSDATE, DBMS_UTILITY.FORMAT_ERROR_STACK);
END XXX_ERROR;

※SELECT * FROM III;給一個沒有的table名稱就會產生錯誤



※啟用/禁用Trigger

.預設在編譯完trigger後,就可以用了,可以在ON table 加上DISABLE或ENABLE(無D),也就是預設在ON table後面就有ENABLE了,所以可以不打

.user_triggers、all_triggers、dba_triggers(sys權限)這三張表可以看全部的trigger,如看狀態可以用SELECT status FROM user_triggers就可以看到ENABLED或DISABLED(有D)

也可以使用ALTER TRIGGER trigger_name DISABLE/ENABLE,針對特定的trigger啟/禁用

.還可以使用ALTER TABLE [schema.]table_name ENABLE/DISABLE ALL TRIGGERS,如ALTER TABLE emp ENABLE ALL TRIGGERS;

Trigger四(DML trigger四 view) (PL/SQL 二十一)

針對view的trigger,主要是因為view有多表的時候不能增刪改,所以使用view trigger可以達到,但如果有彙總函數、CASE、DECODE、GROUP BY、DISTINCT還是不行,稍微想一下就知道為什麼不行了
官網連結


※新增一個view來練習

CREATE OR REPLACE VIEW xxx_view AS
SELECT
    e.EMPNO, e.ENAME, d.DEPTNO, d.DNAME
FROM EMP e, DEPT d
WHERE
    e.DEPTNO = d.DEPTNO
AND d.DEPTNO = 30;

※Oracle11g和12c預設是沒有權限可以新增view的,用有權限的帳號進入後,打上GRANT CREATE VIEW TO 使用者名稱;才有權限

※如果記不起來可以用視窗的方式來新增
 使用畫面可以輕鬆完成,完成後編譯,如下圖可以看見

※一執行「INSERT INTO xxx_view VALUES(7777, 'aaa', 50, 'xxx');」
ORA-01776: cannot modify more than one base table through a join view就會出錯,所以才要有view trigger



※新增

CREATE OR REPLACE TRIGGER update_view_trigger
    INSTEAD OF INSERT ON xxx_view 
    FOR EACH ROW
DECLARE
    xxx_emp_count NUMBER;
    xxx_dept_count NUMBER;
BEGIN
    SELECT COUNT(*) INTO xxx_emp_count FROM EMP
    WHERE empno = :new.empno;
    
    SELECT COUNT(*) INTO xxx_dept_count FROM DEPT
    WHERE deptno = :new.deptno;
    
    IF xxx_emp_count = 0 THEN
        INSERT INTO emp(empno, ename) VALUES(:new.empno, :new.ename);
    END IF;
    
    IF xxx_dept_count = 0 THEN
        INSERT INTO dept(deptno, dname) VALUES(:new.deptno, :new.dname);
    END IF;
END update_view_trigger;

※再次執行「INSERT INTO xxx_view VALUES(7777, 'aaa', 50, 'xxx');」,看xxx_view、emp、dept可以發現成功了

※其實很多都有畫面操作,在Trigger按右鍵新增,如下



※修改

CREATE OR REPLACE TRIGGER update_view_trigger
    INSTEAD OF UPDATE ON xxx_view 
    FOR EACH ROW
BEGIN
    UPDATE emp SET ename = :new.ename WHERE empno = :new.empno;
    UPDATE dept SET dname = :new.dname WHERE deptno = :new.deptno;
END update_view_trigger;

※執行「UPDATE xxx_view SET ename = 'bbb', dname='ccc' WHERE empno = 7844;」


※刪除

CREATE OR REPLACE TRIGGER update_view_trigger
    INSTEAD OF DELETE ON xxx_view 
    FOR EACH ROW
DECLARE
    xxx_emp_count NUMBER;
BEGIN
    DELETE FROM emp WHERE empno = :old.empno;
    
    SELECT COUNT(*) INTO xxx_emp_count FROM EMP
    WHERE deptno = :old.deptno;
    
    IF xxx_emp_count = 0 THEN
        DELETE FROM dept WHERE deptno = :old.deptno;
    END IF;
END update_view_trigger;

※執行「DELETE FROM xxx_view WHERE deptno = 30;」


※增刪改

也就是將剛剛的新增、修改、刪除合併成一支


CREATE OR REPLACE TRIGGER update_view_trigger
    INSTEAD OF INSERT OR UPDATE OR DELETE ON xxx_view 
    FOR EACH ROW
DECLARE
    xxx_emp_count NUMBER;
    xxx_dept_count NUMBER;
BEGIN
    IF INSERTING THEN
        SELECT COUNT(*) INTO xxx_emp_count FROM EMP
        WHERE empno = :new.empno;
    
        SELECT COUNT(*) INTO xxx_dept_count FROM DEPT
        WHERE deptno = :new.deptno;
    
        IF xxx_dept_count = 0 THEN
            INSERT INTO dept(deptno, dname) VALUES(:new.deptno, :new.dname);
        END IF;
    
        IF xxx_emp_count = 0 THEN
            INSERT INTO emp(empno, ename) VALUES(:new.empno, :new.ename);
        END IF;
    ELSIF UPDATING THEN
        UPDATE emp SET ename = :new.ename WHERE empno = :new.empno;
        UPDATE dept SET dname = :new.dname WHERE deptno = :new.deptno;
    ELSIF DELETING THEN
        DELETE FROM emp WHERE empno = :old.empno;
    
        SELECT COUNT(*) INTO xxx_emp_count FROM EMP
        WHERE deptno = :old.deptno;
    
        IF xxx_emp_count = 0 THEN
            DELETE FROM dept WHERE deptno = :old.deptno;
        END IF;
    END IF;
END update_view_trigger;



※view和巢狀表


※事前準備

DROP TYPE xxx_nested;
DROP TYPE xxx_type;
CREATE OR REPLACE TYPE xxx_type IS OBJECT(
    pid NUMBER,
    pname VARCHAR2(20),
    pdate DATE
);
    
-- 新增巢狀表,型態是自定的,叫xxx_type
CREATE OR REPLACE TYPE xxx_nested IS TABLE OF xxx_type NOT NULL;
    
DROP TABLE xxx_table PURGE;
CREATE TABLE xxx_table(
    xid NUMBER,
    xname VARCHAR(20) NOT NULL,
    ooo_nested xxx_nested,
    CONSTRAINT vt_pk PRIMARY KEY(xid)
) NESTED TABLE ooo_nested STORE AS PROJECTS_NESTED_TABLE;
    
-- 新增資料來練習
INSERT INTO xxx_table VALUES(1, 'xxx',
    xxx_nested(
        xxx_type(10, 'aaa', TO_DATE('2000-01-01', 'YYYY-MM-DD')),
        xxx_type(20, 'bbb', TO_DATE('2011-11-11', 'YYYY-MM-DD')),
        xxx_type(30, 'ccc', TO_DATE('1988-01-11', 'YYYY-MM-DD'))
    )
);
    
INSERT INTO xxx_table VALUES(2, 'ooo',
    xxx_nested(
        xxx_type(11, 'ddd', TO_DATE('1997-02-22', 'YYYY-MM-DD')),
        xxx_type(22, 'eee', TO_DATE('1996-10-11', 'YYYY-MM-DD'))
    )
);
    
-- 看結果有沒有在裡面
SELECT * FROM xxx_table;
    
-- 新增view
CREATE OR REPLACE VIEW v1 AS
SELECT xid, xname, ooo_nested
FROM xxx_table
WHERE xid = 1;



※增刪改

INSERT INTO TABLE(
    SELECT ooo_nested FROM v1
)VALUES(3, 'fff', TO_DATE('1999-09-09', 'YYYY-MM-DD'));
    
UPDATE TABLE(SELECT ooo_nested FROM v1)xxx
set value(xxx) = xxx_type(
    10, 'fff', TO_DATE('2000-01-01', 'YYYY-MM-DD')
)WHERE xxx.pid = 10;
    
DELETE FROM TABLE(
    SELECT ooo_nested FROM v1
)xxx
WHERE xxx.pid=10;

※執行就會出現「ORA-25015: cannot perform DML on this nested table view column」的錯,雖然和前面的錯誤訊息不同,但反正就是會錯



※view trigger

CREATE OR REPLACE TRIGGER nested_trigger
    INSTEAD OF INSERT OR UPDATE OR DELETE
    ON NESTED TABLE ooo_nested OF v1
-- DECLARE
BEGIN
    IF INSERTING THEN
        INSERT INTO TABLE(
            SELECT ooo_nested FROM xxx_table WHERE xid = :parent.xid
        )VALUES(:new.pid, :new.pname, :new.pdate);
    
    ELSIF UPDATING THEN
        UPDATE TABLE(
            SELECT ooo_nested FROM xxx_table WHERE xid = :parent.xid
        )xxx
        SET VALUE(xxx) = xxx_type(
            :new.pid, :new.pname, :new.pdate
        )WHERE xxx.pid = :old.pid;
    
    ELSIF DELETING THEN
        DELETE FROM TABLE(
            SELECT ooo_nested FROM xxx_table WHERE xid = :parent.xid
        )xxx
        WHERE xxx.pid = :old.pid;
    END IF;
END;

※:new和:old控制的是自定型態的表,也就是xxx_type; :parent控制的是有巢狀欄位的表,也就是xxx_table,可以想成在insert時給的是pid、pname、pdate,而它的爸爸自然就是xxx_table了,所以控制的是xid、xname,對照下面幾行比較清楚明白
xxx_type-->pid、pname、pdate==>:new、:old
xxx_nested -->xxx_type
xxx_table(v1)-->xid、xname、ooo_nested(xxx_nested)==>:parent

※再次執行增刪改,然後再看v1或xxx_table就會發現成功了

2016年2月26日 星期五

Trigger三(DML trigger三 表、行級trigger) (PL/SQL 二十)

也就是將行級和表級合成一個trigger,之前的做法要寫四支trigger(before、after+行、表級)
分成四種,可看官網的解釋,稍為往下一點的Table 9-1


※基本語法

CREATE OR REPLACE TRIGGER LINE_TABLE_TRIGGER
    FOR INSERT OR UPDATE OR DELETE ON DEPT
    COMPOUND TRIGGER
    
    BEFORE STATEMENT IS
    BEGIN
        DBMS_OUTPUT.PUT_LINE('表級之前');
    END BEFORE STATEMENT;
    
    AFTER STATEMENT IS
    BEGIN
        DBMS_OUTPUT.PUT_LINE('表級之後');
    END AFTER STATEMENT;
    
    BEFORE EACH ROW IS
    BEGIN
        DBMS_OUTPUT.PUT_LINE('行級之前');
    END BEFORE EACH ROW;
    
    AFTER EACH ROW IS
    BEGIN
        DBMS_OUTPUT.PUT_LINE('行級之後');
    END AFTER EACH ROW;
END;

※執行「INSERT INTO DEPT VALUES (50, 'a', 'b');」可以出現結果

※結果:
表級之前
行級之前
行級之後
表級之後



※使用:new、:old

CREATE OR REPLACE TRIGGER LINE_TABLE_TRIGGER
    FOR INSERT OR UPDATE OR DELETE ON DEPT
    COMPOUND TRIGGER
    
    BEFORE EACH ROW IS
    BEGIN
        IF INSERTING THEN
            IF :new.dname IS NULL THEN
                :new.dname := 'default';
            END IF;
    
            IF :new.loc IS NULL THEN 
                :new.loc := '芝加哥';
            END IF;
        END IF;
    END BEFORE EACH ROW;
END;

※INSERT INTO DEPT(DEPTNO) VALUES(50);

※當然還是只有行級才能用



※宣告變數

CREATE OR REPLACE TRIGGER xxx
    FOR INSERT OR UPDATE OR DELETE ON EMP
    COMPOUND TRIGGER
    
    BEFORE EACH ROW IS
        xxx_sal emp.sal%TYPE;
    BEGIN
        IF INSERTING OR UPDATING THEN
            :new.ename := UPPER(:new.ename);
        END IF;
    
        IF INSERTING THEN
            SELECT AVG(sal) INTO xxx_sal FROM EMP;
            IF :new.sal > xxx_sal THEN
                &RAISE_APPLICATION_ERROR(-20000, '不可高於平均工資');
            END IF;
        END IF;
    END BEFORE EACH ROW;
END;

※INSERT INTO EMP(EMPNO, ENAME, SAL) VALUES(9999, 'test', 8000);

※寫在IS和BEGIN之間即可

2016年2月24日 星期三

Trigger二(DML trigger二 REFERENCING、WHEN、增刪改ING、FOLLOWS) (PL/SQL 十九)

※REFERENCING

預設的:new和:old可以改成自己喜歡的,可以只改一個
CREATE OR REPLACE TRIGGER line_trig_update
    BEFORE UPDATE OF SAL ON EMP
    REFERENCING
        new AS ooo
        old AS xxx
    FOR EACH ROW
DECLARE
BEGIN
    IF ABS(:ooo.sal - :xxx.sal) >= 1000 THEN
        RAISE_APPLICATION_ERROR(-20000, '加薪太過!');
    END IF;
END;

※如上new改成ooo;old改成xxx,所以new和old就不能用了,AS可省略



※WHEN

trigger被觸發之後會判斷這裡設定的條件
CREATE OR REPLACE TRIGGER line_trig_update
    BEFORE UPDATE OF SAL ON EMP FOR EACH ROW
    WHEN (new.sal <= old.sal)
BEGIN
    RAISE_APPLICATION_ERROR(-20000, '新的薪水<=舊的薪水');
END;

※要注意使用WHEN時,new和old,不能加前面的「:」



※增刪改ING

Oracle提供三個內鍵的變數,可以判斷增刪改,分別是INSERTING、UPDATING、DELETING
CREATE OR REPLACE TRIGGER line_trig_update
    BEFORE INSERT OR UPDATE OR DELETE ON DEPT
    -- FOR EACH ROW
BEGIN
    IF INSERTING THEN
        RAISE_APPLICATION_ERROR(-20000, '新增');
    ELSIF UPDATING THEN
        RAISE_APPLICATION_ERROR(-20001, '修改');
    ELSIF DELETING THEN
        RAISE_APPLICATION_ERROR(-20002, '刪除');
    END IF;
END;

※行、表級都可以用,BEFORE後面的INSERT等還是要寫



※FOLLOWS

多個觸發器的條件都一樣時,順序不一定會按照我們想的去做,所以可以用FOLLOWS
CREATE OR REPLACE TRIGGER dept1
    BEFORE INSERT ON DEPT FOR EACH ROW
BEGIN
    DBMS_OUTPUT.PUT_LINE('111');
END;
    
CREATE OR REPLACE TRIGGER dept2
    BEFORE INSERT ON DEPT FOR EACH ROW
    -- FOLLOWS dept1
BEGIN
    DBMS_OUTPUT.PUT_LINE('222');
END;
    
CREATE OR REPLACE TRIGGER dept3
    BEFORE INSERT ON DEPT FOR EACH ROW
    -- FOLLOWS dept2
BEGIN
    DBMS_OUTPUT.PUT_LINE('333');
END;

※如上有三個觸發器,我新增時的順序是321,但我想要123,所以將上面的註解打開即可

※FOLLOWS後面接trigger名稱,後面接的trigger觸發完後就會換自己了

※如果2和3都接1不會錯,只是就和沒設定FOLLOWS一樣,順序不確定,沒意義了

2016年2月20日 星期六

Trigger一(DML trigger一 增刪改、子事務、自增主鍵) (PL/SQL 十八)

※Trigger就是執行了某件事後,會自動做某件事的動作

※DML trigger針對增刪改的之前或之後,自動執行一些程式,分成表級和行級



※表級DML trigger

※針對表的trigger

CREATE OR REPLACE TRIGGER trigger_test
    BEFORE INSERT OR DELETE ON DEPT
DECLARE
    xxx_min NUMBER;
    xxx_week NUMBER;
BEGIN
    SELECT 
        TO_NUMBER(TRIM(TO_CHAR(SYSDATE, 'D'))), 
        TO_NUMBER(TRIM(TO_CHAR(SYSDATE, 'HH24MI')))
    INTO xxx_week, xxx_min FROM DUAL;
    
    IF xxx_week = 1 OR xxx_week = 7 THEN
        RAISE_APPLICATION_ERROR(-20000, '星期六日不允許更動DEPT表');
    ELSIF xxx_min < 900 OR xxx_min > 1830 THEN
        RAISE_APPLICATION_ERROR(-20001, '非上班時間不允許更動DEPT表');
    END IF;
END;

※執行「INSERT INTO DEPT VALUES(80, 'a', 'b');」因為是BEFORE,所以會在執行insert之前就去判斷能不能新增

※1代表星期日,7代表星期六



※針對表裡的欄位trigger

CREATE OR REPLACE TRIGGER trigger_test
BEFORE UPDATE OF LOC ON DEPT
DECLARE
    xxx_hour NUMBER;
BEGIN
    SELECT TO_NUMBER(TRIM(TO_CHAR(SYSDATE, 'HH24')))
    INTO xxx_hour FROM DUAL;
    
    IF xxx_hour >= 12 THEN
        RAISE_APPLICATION_ERROR(-20001, '12點之後不允許修改DEPT表的LOC欄位');
    END IF;
END;

※因為是針對DEPT表的LOC欄位,所以「UPDATE DEPT SET DNAME = 'xxx' WHERE DEPTNO = 80;」可以成功修改,但「UPDATE DEPT SET LOC = 'ooo' WHERE DEPTNO = 80;」如果過12點就會跳出trigger



※子事務

※trigger裡也可以有事務


※新增一張表做練習

CREATE TABLE after_emp_sal(
    EMPNO NUMBER(4),
    ENMAE VARCHAR2(10),
    SAL NUMBER(7,2),
    COMM NUMBER(7,2),
    AFTER_SAL NUMBER(7,2),
    CONSTRAINT AFTER_PK PRIMARY KEY(EMPNO)
);

※這張表是在做EMP的SAL(薪水)和COMM(佣金)欄位,加總後判斷多少錢,然後依加薪的程度有所不同,最後新增到這張表



※先不開啟子事務

CREATE OR REPLACE TRIGGER add_sal
    AFTER INSERT OR UPDATE OF SAL, COMM ON EMP
DECLARE
    -- PRAGMA AUTONOMOUS_TRANSACTION;
    CURSOR xxx_cur IS SELECT * FROM EMP;
    xxx_emp EMP%ROWTYPE;
    xxx_sal EMP.SAL%TYPE;
    xxx_after_sal after_emp_sal.after_sal%type;
BEGIN
    FOR xxx_emp IN xxx_cur LOOP
        xxx_sal := NVL(xxx_emp.sal, 0) + NVL(xxx_emp.comm, 0);
    
        IF xxx_sal < 2000 THEN
            xxx_after_sal := xxx_sal * 1.3;
        ELSIF xxx_sal <= 3000 THEN
            xxx_after_sal := xxx_sal * 1.2;
        ELSIF xxx_sal <= 4000 THEN
            xxx_after_sal := xxx_sal * 1.1;
        END IF;
    
        INSERT INTO after_emp_sal values(
            xxx_emp.empno,
            xxx_emp.ename,
            xxx_emp.sal,
            xxx_emp.comm,
            xxx_after_sal
        );
    END LOOP;
    -- COMMIT;
END;

※執行「INSERT INTO EMP (empno, ename, sal, comm) VALUES(8888, 'xxx', 2400, 200);」之後就會發現after_emp_sal已經新增了,也包含了8888這筆,但如果開啟另外一個session,就會發現是空的,因為沒有COMMIT,所以COMMIT或ROLLBACK後,就會全看的見或全看不見

※如果只開啟trigger的COMMIT,就會出ORA-04092:cannot COMMIT in a trigger錯誤訊息,說trigger裡,不能有COMMIT,這時再開啟PRAGMA那行的子事務就可以了

※兩者最大的差異在8888新增的這一筆,如果是用子事務,就不會包括

※如果有子事務,然後把INSERT那筆rollback後,after_emp_sal還是有資料(當然沒有8888,要等下次再做一次才有)



※行級DML trigger

只有加上FOR EACH ROW才表示行級DML trigger,也才可以使用:new、:old

官網有寫:new和:old的表,新增的:old和刪除的:new都是null



※新增測試

CREATE OR REPLACE TRIGGER line_trig_add
    BEFORE INSERT ON DEPT FOR EACH ROW
DECLARE
    xxx_count NUMBER;
    xxx_deptno NUMBER;
BEGIN
    SELECT COUNT(*) INTO xxx_count FROM DEPT 
    WHERE :new.loc IN(
        SELECT LOC FROM DEPT
    );
    
    IF xxx_count = 0 THEN
        RAISE_APPLICATION_ERROR(-20000, '公司沒有這個地點!');
    ELSE
        xxx_deptno := :new.deptno;
        IF SUBSTR(xxx_deptno, LENGTH(xxx_deptno), 1) != 0 OR xxx_deptno <= 0 THEN
            RAISE_APPLICATION_ERROR(-20001, '部門號必需是10的倍數!');
        END IF;
    END IF;
END;

※INSERT INTO DEPT VALUES(80, 'xxx', 'ooo');
INSERT INTO DEPT VALUES(62, 'xxx', 'CHICAGO');
以上兩條分別出現不一樣的trigger



※修改測試

CREATE OR REPLACE TRIGGER line_trig_update
    BEFORE UPDATE OF SAL ON EMP FOR EACH ROW
DECLARE
BEGIN
    IF ABS(:new.sal - :old.sal) >= 1000 THEN
        RAISE_APPLICATION_ERROR(-20000, '加薪太過!');
    END IF;
END;

※如果加薪幅度超過1000就會跳出trigger



※刪除測試

CREATE OR REPLACE TRIGGER line_trig_delete
    BEFORE DELETE ON DEPT FOR EACH ROW
DECLARE
BEGIN
    IF :old.deptno = 10 THEN
        RAISE_APPLICATION_ERROR(-20000, '無法刪除10部門');
    END IF;
END;

※只有刪除10部門才會跳出trigger



※注意事項

1.宣告ddd DEPT%ROWTYPE;
然後使用ddd := :new;是不可以的,雖然:new或:old很像%ROWTYPE,但是是不可以的

2.宣告ooo NUMBER;
然後使用ooo := :old.sal;這也是不行的,原始值是不能被修改的,但:new是可以的,如上面的新增就有用到

3.上面的程式碼是我自己run成功的結果貼上去的,有一次我自己打開PL/SQL Developer,然後打開一個新的Test Window,然後複製我成功的程式碼,居然出現「ORA-01036: illegal variable name/number」,debug後發現是:new、:old的錯,也就是它認為我不能有「:」開頭的變數,見鬼了,後來我try了好久,發現有這種「:」開頭的,可以先把它刪除,變成new和old然後編譯就會過了,但當然是不能run,所以我去trigger按右鍵Edit或在command輸入edit trigger名稱就會跳出來,然後再將「:」加上去,最後編譯就可以了

4.編譯完trigger後,就不能更改別的表了,如ON DEPT,就不能換成別的,否則會出現「ORA-04095:trigger 'trigger名稱' already exists on another table, cannot replace it」的錯,只能先Drop這個trigger或重新寫一支trigger,名稱不一樣就ok



※自增主鍵

Oracle12c有提供和其他家資料庫一樣,不用再做Sequence,以下先說不是12c時的做法

※建立兩張表做練習

CREATE SEQUENCE ttt_sequence;
    
CREATE TABLE trig_test_table(
    id NUMBER,
    name VARCHAR2(10),
    CONSTRAINT ttt_pk PRIMARY KEY(id)
);
    
CREATE TABLE trig_test_table_copy AS SELECT * FROM trig_test_table WHERE 1=0;

※where 1=0是因為假設trig_test_table有10筆資料,而因為where條件不成立,所以只會複製表的結構而已



※trigger

CREATE OR REPLACE TRIGGER trigger_test
    BEFORE INSERT ON trig_test_table_copy FOR EACH ROW
DECLARE
    xxx_ttt trig_test_table%ROWTYPE;
BEGIN
    DELETE FROM trig_test_table_copy;
    SELECT ttt_sequence.NEXTVAL INTO :new.id FROM DUAL;
    
    xxx_ttt.id := :new.id;
    xxx_ttt.name := :new.name;
    
    INSERT INTO trig_test_table VALUES xxx_ttt;
END;

※也可以不用%ROWTYPE,直接用一般的INSERT,如下:
CREATE OR REPLACE TRIGGER trigger_test
    BEFORE INSERT ON trig_test_table_copy FOR EACH ROW
BEGIN
    DELETE FROM trig_test_table_copy;
    INSERT INTO VALUES(ttt_sequence.NEXTVAL, :new.name);
END;

※因為不用宣告變數,所以連DECLARE也不要了,比較簡潔

※insert into trig_test_table_copy(name) values('xxx');就可以看到trig_test_table新增進去了

※多一張表是因為如果沒有copy那張表,那第一次insert時觸發了Trigger,而Trigger裡面因為沒有copy那張表,是自己的表,那又觸發了Trigger,所以會變成無限迴圈


※Oracle12c的自增主鍵


CREATE TABLE trig_test_table(
    -- id NUMBER,
    id NUMBER GENERATED BY DEFAULT AS IDENTITY (START WITH 1 INCREMENT BY 1),
    name VARCHAR2(10),
    CONSTRAINT ttt_pk PRIMARY KEY(id)
);

※註解的部分變成下面那一行,其實也就是將原本的SEQUENCE寫在裡面,和table挷起來而已,圓括號裡的參數可以不打,參數用法都和SEQUENCE一樣

※這時連trigger都不用了

2016年2月19日 星期五

包三(DBMS_JOB、DBMS_ASSERT) (PL/SQL 十七)

※DBMS_JOB

官方手冊在這
其實就是排程,看要自動做什麼事情的功能



※我試過每一秒就執行一次,使用DBMS_OUT.PUT_LINE但看不出效果,所以就用個測試表來練習,才看得出來
DROP TABLE dbms_job_test_table PURGE;
DROP SEQUENCE dbms_job_test;
    
CREATE TABLE dbms_job_test_table(
    jid NUMBER,
    jname VARCHAR2(20),
    jdate DATE,
    CONSTRAINT jid_pk PRIMARY KEY(jid)
);
    
CREATE SEQUENCE dbms_job_test;



※使用procedure做一個新增的功能

CREATE OR REPLACE PROCEDURE
    save_dbms_job_test_table_proc(p_name dbms_job_test_table.jname%TYPE) IS 
BEGIN
    INSERT INTO dbms_job_test_table VALUES (dbms_job_test.NEXTVAL, p_name, SYSDATE);
END;



※使用排程,參數的用途請看官網的解釋

DECLARE
    num NUMBER;
BEGIN
    DBMS_JOB.SUBMIT(
        num,
        'save_dbms_job_test_table_proc(''xxx'');',
        SYSDATE,
        'SYSDATE + (1/(60*60*24))'
    );
    
    DBMS_OUTPUT.PUT_LINE('num=' || num);
    COMMIT;
END;

※結果:num=24

※要注意第二個參數後面有個「;」,而結果的24視個人情況而定

※使用「SELECT * FROM dbms_job_test_table;」過個幾秒執行一次就會看出效果

※這裡也看得到排程的程式



※刪除排程

DECLARE
BEGIN
    DBMS_JOB.REMOVE(24);
    DBMS_OUTPUT.PUT_LINE('刪除job成功');
    COMMIT;
END;

※執行了以後就不會再新增了

※但有時候可能會忘記24這個數字,所以有一張表「user_jobs」,可以看到JOB這個欄位,如果還沒REMOVE可以看得到24這個數字,REMOVE完就沒了,也就證明刪除了




※DBMS_ASSERT

這個包可以將字串做一些驗證或轉換,可參考官網


※ENQUOTE_LITERAL、ENQUOTE_NAME

SELECT
    DBMS_ASSERT.ENQUOTE_LITERAL('xxx') as A,
    DBMS_ASSERT.ENQUOTE_NAME('xxx') as B
FROM DUAL;

※結果:
'xxx'
"XXX"

※ENQUOTE_LITERAL會在前後加上「'」

※ENQUOTE_NAME會將字串變成大寫,然後會在前後加上「"」



※SQL_OBJECT_NAME、QUALIFIED_SQL_NAME、SIMPLE_SQL_NAME

SELECT
    DBMS_ASSERT.SQL_OBJECT_NAME('xxx') as C,
    DBMS_ASSERT.QUALIFIED_SQL_NAME('xxx') as D,
    DBMS_ASSERT.SIMPLE_SQL_NAME('xxx') as E
FROM DUAL;

※這三個方法如果都正確,都會回傳字串

※SQL_OBJECT_NAME是找有沒有符合的物件,例如叫xxx的function或procedure

※QUALIFIED_SQL_NAME、SIMPLE_SQL_NAME是指xxx是合法的SQL名稱嗎?所以如果輸入123xxx,因為是數字開頭,所以會錯,就不能用123xxx當變數,兩個我分不太清楚差在哪裡,只知道SIMPLE_SQL_NAME不檢查長度



※SCHEMA_NAME、NOOP

SELECT
    DBMS_ASSERT.SCHEMA_NAME('DIP') as F,
    DBMS_ASSERT.NOOP('xxx') as G
FROM DUAL;

※NOOP輸入什麼就返回什麼,類似dos的echo

※SCHEMA_NAME裡的字串一定是大寫,驗證是不是有效的SCHEMA,也可用滑鼠看,但Oracle SQL Developer看不到自己的

2016年2月14日 星期日

包二(限制包的行為、DBMS_OUTPUT包) (PL/SQL 十六)

※限制包的行為

可參考官方網站
常用的有四種:
WNDS:Writes No Database State-->不能寫入資料庫的狀態,所以不能新增、修改、刪除
RNDS:Reads No Database State-->不能讀資料庫的狀態,所以不能查詢
WNPS:Writes No Package State-->不能寫入包的狀態,所以不能賦值,如var := 'x';
RNPS:Reads No Package State-->不能讀包的狀態,所以不能使用變數


CREATE OR REPLACE PACKAGE xxx_pkg IS
    ooo VARCHAR2(10) := 'ooo';
    
    FUNCTION a(p NUMBER) RETURN NUMBER;
    FUNCTION b(p VARCHAR2) RETURN NUMBER;
    FUNCTION c(p1 NUMBER, p2 VARCHAR2) RETURN VARCHAR2;
    FUNCTION d(p1 VARCHAR2, p2 NUMBER) RETURN VARCHAR2;
    
    PRAGMA RESTRICT_REFERENCES(a, WNDS);
    PRAGMA RESTRICT_REFERENCES(b, RNDS);
    PRAGMA RESTRICT_REFERENCES(c, WNPS);
    PRAGMA RESTRICT_REFERENCES(d, RNPS);
END;

※定義四個限制包的行為


CREATE OR REPLACE PACKAGE BODY xxx_pkg IS
    FUNCTION a(p NUMBER) RETURN NUMBER IS
    BEGIN
    DELETE FROM DEPT WHERE DEPTNO = p;
    RETURN 0;
    END a;
    
    FUNCTION b(p VARCHAR2) RETURN NUMBER IS
    xxx DEPT%ROWTYPE;
    BEGIN
    SELECT * INTO xxx FROM DEPT WHERE DNAME = p;
    RETURN 0;
    END b;
    
    FUNCTION c(p1 NUMBER, p2 VARCHAR2) RETURN VARCHAR2 IS
    BEGIN
    ooo := 'xxx';
    RETURN '';
    END c;
    
    FUNCTION d(p1 VARCHAR2, p2 NUMBER) RETURN VARCHAR2 IS
    BEGIN
    RETURN ooo;
    END d;
END;

※包體故意違反後編譯,就會出現如下的錯誤
Compilation errors for PACKAGE BODY OOO.XXX_PKG

Error: PLS-00452: Subprogram 'A' violates its associated pragma
Line: 3
Text: FUNCTION a(p NUMBER) RETURN NUMBER IS

Error: PLS-00452: Subprogram 'B' violates its associated pragma
Line: 9
Text: FUNCTION b(p VARCHAR2) RETURN NUMBER IS

Error: PLS-00452: Subprogram 'C' violates its associated pragma
Line: 16
Text: FUNCTION c(p1 NUMBER, p2 VARCHAR2) RETURN VARCHAR2 IS

Error: PLS-00452: Subprogram 'D' violates its associated pragma
Line: 22
Text: FUNCTION d(p1 VARCHAR2, p2 NUMBER) RETURN VARCHAR2 IS

Error: Hint: Parameter 'p1' is declared but never used in 'c'
Line: 16
Text: FUNCTION c(p1 NUMBER, p2 VARCHAR2) RETURN VARCHAR2 IS

Error: Hint: Parameter 'p2' is declared but never used in 'c'
Line: 16
Text: FUNCTION c(p1 NUMBER, p2 VARCHAR2) RETURN VARCHAR2 IS

Error: Hint: Parameter 'p1' is declared but never used in 'd'
Line: 22
Text: FUNCTION d(p1 VARCHAR2, p2 NUMBER) RETURN VARCHAR2 IS

Error: Hint: Parameter 'p2' is declared but never used in 'd'
Line: 22
Text: FUNCTION d(p1 VARCHAR2, p2 NUMBER) RETURN VARCHAR2 IS

※也可以限制只能讀取資料庫等,如:
PRAGMA RESTRICT_REFERENCES(a, WNDS, WNPS, RNPS);
也就是用逗號隔開



※DBMS_OUTPUT包

※PUT_LINE、ENABLE、DISABLE、PUT、NEW_LINE

DECLARE
BEGIN
    DBMS_OUTPUT.PUT_LINE('目前看得見');
    DBMS_OUTPUT.DISABLE;
    DBMS_OUTPUT.PUT_LINE('看不見');
    
    DBMS_OUTPUT.ENABLE;
    DBMS_OUTPUT.PUT_LINE('看得見');
    
    DBMS_OUTPUT.PUT('ww');
    DBMS_OUTPUT.PUT('w.');
    DBMS_OUTPUT.NEW_LINE;
    DBMS_OUTPUT.PUT('因為後面沒有執行NEW_LINE,所以看不見');
END;

※結果:
看得見
www.

※DISABLE方法是將整個輸出關閉,不管之前有沒有,一切全洗掉,類似SET SERVEROUTPUT OFF

※PUT後面必需有接換行的方法才會印出,如NEW_LINE、PUT_LINE

※debug時,寫DBMS_OUTPUT.PUT_LINE寫太多時報錯,可用exception印SQLCODE,有可能是報「-20000」的錯,因為超過buffer大小,預設是20000,可參考官網

以下是不設定 buffer 的做法:
declare 
begin
    DBMS_OUTPUT.ENABLE(null);
    for i in 1..1000 loop
        dbms_output.put_line('1234567890');
    end loop;
end;

※給 null 和空都可以,或者用以下的方式

※如果兩個都設定,以程式碼為主



※GET_LINE

DECLARE
    line1 VARCHAR2(20);
    line2 VARCHAR2(20);
    status INTEGER;
BEGIN
    DBMS_OUTPUT.ENABLE;
    
    DBMS_OUTPUT.PUT('http:');
    DBMS_OUTPUT.PUT('//');
    DBMS_OUTPUT.NEW_LINE;
    
    DBMS_OUTPUT.PUT('www.');
    DBMS_OUTPUT.PUT('google.com');
    DBMS_OUTPUT.NEW_LINE;
    
    DBMS_OUTPUT.GET_LINE(line1, status);
    -- DBMS_OUTPUT.GET_LINE(line2, status);
    
    DBMS_OUTPUT.PUT_LINE('line1=' || line1);
    -- DBMS_OUTPUT.PUT_LINE('line2=' || line2);
    
    DBMS_OUTPUT.PUT_LINE('status=' || status);
END;

※結果:
l1=http://
xxx_out=0

※一直到最後的NEW_LINE是上一個程式碼講過的,但一執行到GET_LINE就什麼都印不出來了,然後要到PUT_LINE又會印出,印出的是第一次NEW_LINE上面的全部內容,也就是http和//兩行,而最後的xxx_out只有兩個值,1和0,0表示成功印出,1表示一行都沒有,可參考官方網站,所以將ENABLE到第二次的NEW_LINE註解就會印出1了


※GET_LINES、CHARARR

DECLARE
    lines DBMS_OUTPUT.CHARARR;
    -- lines DBMSOUTPUT_LINESARRAY;
    
    numlines INTEGER := 2;
BEGIN
    DBMS_OUTPUT.ENABLE;
    
    DBMS_OUTPUT.PUT('http:');
    DBMS_OUTPUT.PUT('//');
    DBMS_OUTPUT.NEW_LINE;
    
    DBMS_OUTPUT.PUT('www.');
    DBMS_OUTPUT.PUT('google.com');
    DBMS_OUTPUT.NEW_LINE;
    
    DBMS_OUTPUT.GET_LINES(lines, numlines);
    FOR i IN lines.FIRST..lines.LAST LOOP
        DBMS_OUTPUT.PUT_LINE(lines(i));
    END LOOP;
END;

※如果將numlines設為1就等同於GET_LINE

※註解那行也可以,官網有寫

2016年2月13日 星期六

包一(宣告、刪除、重載和Session) (PL/SQL 十五)

※包

.可以將procedure、function、cursor…等包起來一起管理,分成包和包體

.可以將包想成java的interface,而包體就是實作interface的class

.如果包沒定義,而包體寫了,外面就沒辦法訪問,就像是class雖然實作了interface,但裡面又定義了private的方法一樣,只有自己能調用



※宣告包

CREATE OR REPLACE PACKAGE xxx_pkg IS
    -- TYPE xxx_cur IS REF CURSOR;
    FUNCTION dept_of_emp(xxx_dno DEPT.DEPTNO%TYPE)
    RETURN SYS_REFCURSOR;
    -- RETURN xxx_cur;
    
    -- PROCEDURE aaa;
    -- CURSOR bbb RETURN DEPT%ROWTYPE;
    -- CURSOR ccc RETURN DEPT%ROWTYPE IS
    -- SELECT * FROM DEPT WHERE DEPTNO = 20;
END;

※上面是系統定義的弱游標類型,如果要自己定義弱游標,就將第一個註解打開,然後回傳值就是自定義的即可(就是第二個註解)

※剩下的註解就是可以定很多的意思,如procedure、function…等,和java不一樣的地方是它也可以定出內容,如最後的ccc,這樣子實作它的包體就可以直接拿來用,不用實作了



※宣告包體

CREATE OR REPLACE PACKAGE BODY xxx_pkg IS
    FUNCTION dept_of_emp(xxx_dno DEPT.DEPTNO%TYPE)
    RETURN SYS_REFCURSOR
    -- RETURN xxx_cur
    
    IS
    ooo_cur SYS_REFCURSOR;
    -- ooo_cur xxx_cur;
    BEGIN
        OPEN ooo_cur FOR SELECT * FROM EMP WHERE DEPTNO = xxx_dno;
        RETURN ooo_cur;
        CLOSE ooo_cur;
    END dept_of_emp;
    
    PROCEDURE aaa IS
    BEGIN
        DBMS_OUTPUT.PUT_LINE('aaa');
    END aaa;
END;

※注意包體第一行變成PACKAGE BODY,有BODY喔!

※註解的部分是自定弱游標的寫法



※測試

DECLARE
    ooo_cur SYS_REFCURSOR;
    -- ooo_cur xxx_pkg.xxx_cur;
    
    xxx_emp EMP%ROWTYPE;
BEGIN
    -- xxx_pkg.aaa;
    ooo_cur := xxx_pkg.dept_of_emp(20);
    
    /*
    FETCH ooo_cur INTO xxx_emp;
    WHILE ooo_cur%FOUND LOOP
        DBMS_OUTPUT.PUT_LINE(xxx_emp.ename);
        FETCH ooo_cur INTO xxx_emp;
    END LOOP;
    */
    
    LOOP
        FETCH ooo_cur INTO xxx_emp;
        EXIT WHEN ooo_cur%NOTFOUND;
        DBMS_OUTPUT.PUT_LINE(xxx_emp.ename);
    END LOOP;
    
    CLOSE ooo_cur;
END;

※如果是自定弱游標的類型,就要寫成「包.游標」

※xxx_pkg.aaa這一行必需在包有定義,不然就是最前面說的,只是個內部能調用的方法

※兩種迴圈都可以用,但我試FOR迴圈會錯,不知道為什麼

※USER_OBJECTS、USER_SOURCE這兩張表可以看到宣告的一些資訊



※刪除包和包體

DROP PACKAGE package名稱;
DROP PACKAGE BODY package名稱;



※重載和Session

就是包裡面可以有同樣的名稱,但參數個數或類型不同
CREATE OR REPLACE PACKAGE xxx_pkg IS
    -- PRAGMA SERIALLY_REUSABLE;
    ooo VARCHAR2(10) := 'ooo';
    
    PROCEDURE xxx(p EMP.ENAME%TYPE);
    PROCEDURE xxx(p EMP.EMPNO%TYPE);
    PROCEDURE xxx(p1 EMP.EMPNO%TYPE, p2 EMP.ENAME%TYPE);
END;

※宣告一個變數和三個procedure



CREATE OR REPLACE PACKAGE BODY xxx_pkg IS
    -- PRAGMA SERIALLY_REUSABLE;
    PROCEDURE xxx(p EMP.ENAME%TYPE) IS
    BEGIN
        DBMS_OUTPUT.PUT_LINE('ename:' || p);
        DBMS_OUTPUT.PUT_LINE('ooo:' || ooo);
    END;
    
    PROCEDURE xxx(p EMP.EMPNO%TYPE) IS
    BEGIN
        DBMS_OUTPUT.PUT_LINE('empno:' || p);
        ooo := 'xxx';
        DBMS_OUTPUT.PUT_LINE('ooo:' || ooo);
    END;
    
    PROCEDURE xxx(p1 EMP.EMPNO%TYPE, p2 EMP.ENAME%TYPE) IS
    BEGIN
        DBMS_OUTPUT.PUT_LINE('2個參數:' || p1 || '和' || p2);
        DBMS_OUTPUT.PUT_LINE('ooo:' || ooo);
    END;
END;

※一個參數的EMPNO改變ooo變數的值



※測試

DECLARE
BEGIN
    -- xxx_pkg.ooo := 'aaa';
    
    -- xxx_pkg.xxx('xyz');
    -- xxx_pkg.xxx(123);
    xxx_pkg.xxx(321, 'zyx');
END;

※重點在xxx_pkg.ooo,如果打開後執行三個隨便一個procedure,然後再關閉xxx_pkg.ooo,這時會發現不是預設的ooo,因為這相當於全域變數的關係,解決方法有兩個,一種就是將視窗關閉再打開執行就可以了,因為視窗關閉,此session就關閉了

另一種方法是將PACKAGE和PACKAGE BODY的註解打開(就是PRAGMA SERIALLY_REUSABLE),這樣就可以每次都幫我們加載-缷載的動作了,不過較浪費效能,所以不建議使用



子程序四(自治事務、Java呼叫子程序) (PL/SQL 十四)

※自治事務

DECLARE
    PROCEDURE XXX IS
    -- PRAGMA AUTONOMOUS_TRANSACTION;
    BEGIN
        INSERT INTO DEPT VALUES(60, 'c', 'd');
        COMMIT;
    END;
BEGIN
    INSERT INTO DEPT VALUES(50, 'a', 'b');
    XXX();
    ROLLBACK;
END;

※註解那一行沒打開時,50和60都會新增成功,因為COMMIT後,ROLLBACK沒用了

※註解打開後,60會新增成功,因為它偵測到自治事務後,將60commit,而外層的50被rollback了



※Java呼叫Procedure

CREATE OR REPLACE
    PROCEDURE xxx(p1 NUMBER, p2 IN OUT NUMBER, p3 OUT NUMBER) IS
BEGIN
    p2 := 5;
    p3 := 10;
END;

※回傳5和10給java接收


※測試類

public class JavaCallProcedure {
    private static final String DRIVER = "oracle.jdbc.driver.OracleDriver";
    private static final String URL = "jdbc:oracle:thin:@127.0.0.1:1521:orcl";
    private static final String USERNAME = "username";
    private static final String PASSWORD = "password";
    
    public static void main(String[] args) throws Exception {
        Class.forName(DRIVER);
        Connection conn = DriverManager.getConnection(URL, USERNAME, PASSWORD);
        CallableStatement cstmt = conn.prepareCall("{ CALL xxx(?, ?, ?)}");
        cstmt.setInt(1, 50);
        cstmt.setInt(2, 100);
        cstmt.registerOutParameter(2, Types.INTEGER);
        cstmt.registerOutParameter(3, Types.INTEGER);
        cstmt.execute();
    
        System.out.println("第二個參數返回值=" + cstmt.getInt(2));
        System.out.println("第三個參數返回值=" + cstmt.getInt(3));
        cstmt.close();
        conn.close();
    }
}

※只能用CALL,我用EXEC會出「java.sql.SQLException: 未支援 SQL92 符號的位置: 3」的錯

※花括號我不加也可以

2016年2月12日 星期五

子程序三(內部子程序、互相調用、重載、和集合配合、遞歸) (PL/SQL 十三)

※內部子程序

※Procedure裡面還有兩個Procedure

CREATE OR REPLACE PROCEDURE dept_ins_proc(
    xxx_dno DEPT.DEPTNO%TYPE,
    xxx_dname DEPT.DNAME%TYPE,
    xxx_loc DEPT.LOC%TYPE,
    xxx_rtn OUT BOOLEAN
) IS
    xxx_count NUMBER;
    PROCEDURE dept_count_proc(
        ooo_dno DEPT.DEPTNO%TYPE,
        ooo_count OUT NUMBER
    )IS
    BEGIN
        SELECT COUNT(DEPTNO) INTO ooo_count FROM DEPT WHERE DEPTNO = ooo_dno;
    END; -- END dept_count_proc;
    
    PROCEDURE dept_ins_do_proc(
        xox_dno DEPT.DEPTNO%TYPE,
        xox_dname DEPT.DNAME%TYPE,
        xox_loc DEPT.LOC%TYPE,
        xox_count NUMBER,
        xox_rtn OUT BOOLEAN
    )IS
    BEGIN
        IF xox_count > 0 THEN
            xox_rtn := FALSE;
        ELSE
            INSERT INTO DEPT VALUES(xox_dno, xox_dname, xox_loc);
            xox_rtn := TRUE;
            COMMIT;
        END IF;
    END;
BEGIN
    DEPT_COUNT_PROC(xxx_dno, xxx_count);
    DEPT_INS_DO_PROC(xxx_dno, xxx_dname, xxx_loc, xxx_count, xxx_rtn);
END;
    
    
    
DECLARE
    rtn BOOLEAN;
BEGIN
    DEPT_INS_PROC(50, '採購部', '芝加哥', rtn);
    IF rtn THEN
        DBMS_OUTPUT.PUT_LINE('true');
    ELSE
        DBMS_OUTPUT.PUT_LINE('false');
    END IF;
END;

※因為使用DBMS印rtn會錯,只好這樣子寫了

※-- END dept_count_proc可以只寫END,但全寫比較清楚是什麼東西結束了

※尤於內部可以訪問外部,所以可以簡化,如下:
CREATE OR REPLACE PROCEDURE dept_ins_proc(
    xxx_dno DEPT.DEPTNO%TYPE,
    xxx_dname DEPT.DNAME%TYPE,
    xxx_loc DEPT.LOC%TYPE,
    xxx_rtn OUT BOOLEAN
) IS
    xxx_count NUMBER;
    PROCEDURE dept_count_proc IS
    BEGIN
        SELECT COUNT(DEPTNO) INTO xxx_count FROM DEPT WHERE DEPTNO = xxx_dno;
    END;
    
    PROCEDURE dept_ins_do_proc IS
    BEGIN
        IF xxx_count > 0 THEN
            xxx_rtn := FALSE;
        ELSE
            INSERT INTO DEPT VALUES(xxx_dno, xxx_dname, xxx_loc);
            xxx_rtn := TRUE;
            COMMIT;
        END IF;
    END;
BEGIN
    DEPT_COUNT_PROC;
    DEPT_INS_DO_PROC();
END;

※此時最後的BEGIN和END就不需要傳參數了

※但子程序裡面的procedure不會出現在資料庫字典裡,所以只有自己能調用,如果需求是自己才需要,那就這樣子寫,不然還是要寫一隻在外面呼叫,所謂資料庫字典就是如下的樣子,圖片是集合的,procedure和function,在左邊是Procedures和Functions,右邊是程序和函數



※互相調用、重載(overloading)

互相調用就是a調用b,b也調用a
重載就是方法名稱一樣,但參數個數或型態不同


※錯誤的例子

CREATE OR REPLACE PROCEDURE xxx(p NUMBER) IS
    -- PROCEDURE b_proc(p2 VARCHAR2);
    PROCEDURE a_proc(p1 NUMBER) IS
    BEGIN
        DBMS_OUTPUT.PUT_LINE('aaa');
        b_proc('yeah!');
    END;
    
    PROCEDURE b_proc(p2 VARCHAR2) IS
    BEGIN
        DBMS_OUTPUT.PUT_LINE('bbb');
        a_proc(1);
    END;
BEGIN
    a_proc(p);
END;
    
    
    
DECLARE
BEGIN
    xxx(10);
END;

※因為a調用b時,b還沒有定義,解法是將上面的註解打開就可以了

※但上面的程式碼沒給結束點,所以是個無限迴圈

※因為有支援重載,而上面的兩個procedure參數類型不同,所以名稱如果都一樣,也是可以執行的



※和集合配合

※以巢狀表為例

DECLARE
    TYPE xxx_nested IS TABLE OF EMP%ROWTYPE;
    xxx xxx_nested;
    
    FUNCTION dept_of_emp(edno EMP.DEPTNO%TYPE) RETURN xxx_nested
    IS
        ooo xxx_nested;
    BEGIN
        SELECT * BULK COLLECT INTO ooo FROM EMP WHERE DEPTNO = edno;
    RETURN ooo;
    END;
BEGIN
    xxx := dept_of_emp(30);
    FOR i IN xxx.FIRST..xxx.LAST LOOP
        DBMS_OUTPUT.PUT_LINE(xxx(i).ENAME);
        DBMS_OUTPUT.PUT_LINE(xxx(i).JOB);
        DBMS_OUTPUT.put_line(xxx(i).SAL || CHR(10));
    END LOOP;
EXCEPTION
    WHEN OTHERS THEN
        DBMS_OUTPUT.PUT_LINE('沒這個部門');
END;

※呼叫完後,跑迴圈印出



※遞歸

就是呼叫自己,所以如果不給一個結束點,就是無限迴圈


※100以下的數字加到100的小程式

DECLARE
    FUNCTION x_to_hundred(i NUMBER) RETURN NUMBER IS
    xxx_sum NUMBER := 0;
    BEGIN
        IF i > 100 THEN
            RETURN xxx_sum;
        ELSE
            xxx_sum := i + x_to_hundred(i + 1);
            RETURN xxx_sum;
        END IF;
    END;
BEGIN
    DBMS_OUTPUT.PUT_LINE(x_to_hundred(1));
END;


子程序二(子程序的table、參數模式、NOCOPY) (PL/SQL 十二)

※子程序的table

.USER_OBJECTS:查看table、index、sequence、procedure、function、type(集合)的大略訊息,而constraint xxx_pk ~~,xxx_pk就是index

.USER_PROCEDURES:查看procedure和function

.USER_TYPES:查看type(集合)

.USER_DEPENDENCIES:査看procedure、function、type(集合)的相依,主要是REFERENCED_NAME這個欄位

.USER_SOURCE:查看Procedure、function、type(集合)的詳細訊息,會將每一行程式碼變成一筆記錄

.USER_ERRORS:查看錯誤訊息

在命令模式(Command Window)裡,可以用view、edit命令,後面接procedure、function、type(集合)、table等名稱就可以開啟相關的畫面,view不能修改,edit可以修改



※參數模式

.IN:只進不出,不能修改,預設值

.OUT:只出不進,傳值進去無效

.IN OUT:又進又出



※IN (Procedure)

CREATE OR REPLACE PROCEDURE in_proc(
    p1 IN VARCHAR2,
    p2 NUMBER
) IS
BEGIN
    DBMS_OUTPUT.PUT_LINE(p1 || '|' || p2);
END;
    
    
    
DECLARE
BEGIN
    in_proc('apple', null);
END;

※上面的null感覺很沒意義,但是不給又會錯,所以可將in_proc修改一下
p2 NUMBER DEFAULT 30,就是在後面多個DEFAULT
這時呼叫端就可以不用給了 in_proc('apple');

※注意順序是VARCHAR2, NUMBER,如果給NUMBER, VARCHAR2就會錯(廢話)
但有個功能是可以的,in_proc(p2=>null, p1=>'apple');這樣子很明顯參數給相反了,p1和p2要和in_proc的參數名對應,然後用「=>」就可以自動對應了



※IN (Function)

CREATE OR REPLACE FUNCTION in_func(
    p1 IN VARCHAR2,
    p2 NUMBER DEFAULT 30
) RETURN VARCHAR2 IS
BEGIN
    DBMS_OUTPUT.PUT_LINE(p1 || '|' || p2);
    RETURN p1;
END;
    
    
    
DECLARE
BEGIN
    DBMS_OUTPUT.PUT_LINE(in_func('apple'));
END;

※如果是在in_func (in_proc也一樣) 將傳進去的p1或p2修改,如「p1 := 'xxx';」就會錯,因為只進不出,不能修改


※OUT

CREATE OR REPLACE PROCEDURE out_proc(
    p1 OUT VARCHAR2,
    p2 OUT NUMBER
) IS
BEGIN
    DBMS_OUTPUT.PUT_LINE(p1 || '|' || p2);
    p1 := 'banana';
    p2 := 30;
END;
    
    
    
DECLARE
    pp1 VARCHAR2(10) := 'xxxx';
    pp2 NUMBER := 20;
BEGIN
    out_proc(pp1, pp2);
    DBMS_OUTPUT.PUT_LINE(pp1 || ',' || pp2);
END;

※DECLARE的參數有參設值,但傳進去的值印出來是空的,因為只出不進

※如果是顯式的呼叫,如「out_proc('apple', 30);」會錯,它一定要傳變數進去



※IN OUT

CREATE OR REPLACE PROCEDURE in_out_proc(
    p1 IN OUT VARCHAR2,
    p2 IN OUT NUMBER
) IS
BEGIN
    DBMS_OUTPUT.PUT_LINE(p1 || '|' || p2);
    p1 := 'banana';
    p2 := 30;
END;
    
    
    
DECLARE
    pp1 VARCHAR2(10) := 'xxxx';
    pp2 NUMBER := 20;
BEGIN
    in_out_proc(pp1, pp2);
    DBMS_OUTPUT.PUT_LINE(pp1 || ',' || pp2);
END;

※IN OUT很像java的傳參考,如傳集合進去,裡面做什麼更動,外面也知道

※它和OUT一樣,一定要傳變數進去,所以「in_out_proc('apple', 30);」還是會錯



※NOCOPY

.IN-->傳進去的值是一個資料庫位址,所以進去的值都不能改,效能較好,所以是預設值,所以也沒有NOCOPY參數

.OUT、IN OUT-->傳進去的值是COPY一份進去的,如果不想COPY,就要加NOCOPY,可以提升效能,不過要注意以下的不同點:

※測試

DECLARE
    xxx NUMBER := 1;
    
    PROCEDURE ooo(p OUT NUMBER) IS
    BEGIN
        DBMS_OUTPUT.PUT_LINE('裡1=' || xxx);
        p := 0;
        DBMS_OUTPUT.PUT_LINE('裡2=' || xxx);
    END;
BEGIN
    ooo(xxx);
    DBMS_OUTPUT.PUT_LINE('外=' || xxx);
END;

OUT:
裡1=1
裡2=1
外=0

OUT NOCOPY:
裡1=
裡2=0
外=0

IN OUT:
裡1=1
裡2=1
外=0

IN OUT NOCOPY:
裡1=1
裡2=0
外=0

※可以看出IN和IN OUT在沒加NOCOPY時,都是一樣的

※加了NOCOPY後,因為過程中不是複制的,所以是空的,這時裡面的值賦值給它,它就會接收

※外面都是0,但要注意如果是EXCEPTION就會接到裡面的值,如下:

DECLARE
    xxx NUMBER := 1;
    
    PROCEDURE ooo(p OUT NOCOPY NUMBER) IS
    BEGIN
        p := 0;
        RAISE_APPLICATION_ERROR(-20000, '結束');
    END;
BEGIN
    BEGIN
        ooo(xxx);
    EXCEPTION
        WHEN OTHERS THEN
            DBMS_OUTPUT.PUT_LINE(xxx);
    END;
END;

※NOCOPY會是0,不加NOCOPY會是1


2016年2月11日 星期四

子程序一(Procedure、Function的宣告和刪除) (PL/SQL 十一)

※Procedure

Procedure和Function最大的差異有兩個
1.Procedure沒有顯式的回傳值(必需用OUT或IN OUT)
2.function的参數和回傳值不能使用 boolean,官網連結


※宣告無參的Procedure

CREATE OR REPLACE PROCEDURE xxx_proc IS 
BEGIN
    DBMS_OUTPUT.PUT_LINE('Hello procedure!');
END;

※OR REPLACE 是可選的,第一次執行可不用,之後要覆寫就一定要打

※無參數時,方法名稱後面不可加「()」

※要呼叫時,PL/SQL Developer可用如下的方式:


※Oracle SQL Developer一樣是DBMS輸出

※寫的時候,無參不能加「()」,但呼叫時,有沒有「()」都可以

※宣告完的程序和函數,就會在上圖左邊的紅框裡

※不可以用select~from的方式



※宣告有參的Procedure

CREATE OR REPLACE PROCEDURE xxx_proc(dno DEPT.DEPTNO%TYPE) IS
    xxx_count NUMBER;
    xxx_dname DEPT.DNAME%TYPE;
    xxx_loc DEPT.LOC%TYPE;
BEGIN
    SELECT COUNT(DEPTNO) INTO xxx_count FROM DEPT WHERE DEPTNO = dno;
    
    IF xxx_count != 0 THEN
        SELECT DNAME, LOC INTO xxx_dname, xxx_loc FROM DEPT WHERE DEPTNO = dno;
        DBMS_OUTPUT.PUT_LINE(xxx_dname || '|' || xxx_loc);
    END IF;
END;




※Function

※宣告Function

CREATE OR REPLACE FUNCTION xxx_func(eno EMP.EMPNO%TYPE) RETURN NUMBER IS
    xxx_sal EMP.SAL%TYPE;
BEGIN
    SELECT SAL + NVL(COMM, 0) INTO xxx_sal FROM EMP WHERE EMPNO = eno;
    RETURN xxx_sal;
END;

※OR REPLACE 是可選的,第一次執行可不用,之後要覆寫就一定要打

※無參數時,方法名稱後面不可加「()」

※一定要寫 RETURN TYPE,否則編譯就錯了

※呼叫時用一般的SQL,如:SELECT xxx_func(7369) FROM DUAL;不可以用exec



※使用PL/SQL呼叫Procedure/Function

上面的呼叫方法為:
.Procedure:使用「EXEC Procedure名稱」; 如果是在PL/SQL Developer,就要在Command Window裡

.Function:一般的SQL下法

.兩者混用就會發生錯誤



以下是用PL/SQL呼叫:

※呼叫Function

DECLARE 
    sal_count NUMBER;
BEGIN
    sal_count := xxx_func(7369);
    DBMS_OUTPUT.PUT_LINE(sal_count);
END;

※宣告一個變數來接並印出

※一定要用回傳值去接,否則會出「'xxx' is not a procedure or is undefined」的錯




※呼叫Procedure

DECLARE 
BEGIN
    xxx_proc(40);
END;

※因沒有回傳值,所以直接印出



※Procedure裡有Function

CREATE OR REPLACE PROCEDURE proc_call_func IS
BEGIN
    DBMS_OUTPUT.PUT_LINE(xxx_func(7369));
END;

※EXEC proc_call_func;



※Fuinction裡有Procedure

CREATE OR REPLACE FUNCTION func_call_proc RETURN BOOLEAN IS
BEGIN
    xxx_proc(40);
    RETURN TRUE;
END;

※SELECT func_call_proc() FROM DUAL;

※使用官方的Oracle SQL Developer,「DBMS輸出」看不出結果,但PL/SQL在DBMS Output活頁標籤看的到,Command Window也都看的到



※刪除Procedure和Function

DROP PROCEDURE procedure名稱;
DROP FUNCTION function名稱;
或者最簡的方法,就是上面的圖,直接選Procedure或Function按右鍵就有很多選擇,其中一個就是DROP

游標二(顯式游標的修改、行級鎖定、動態游標) (PL/SQL 十)

※顯式游標的修改

DECLARE 
    CURSOR xxx_cur IS SELECT * FROM EMP;
BEGIN
    FOR xxx_emp IN xxx_cur LOOP
        IF xxx_emp.deptno = 10 THEN
            IF xxx_emp.sal * 1.2 < 3000 THEN
                UPDATE EMP SET SAL = xxx_emp.sal * 1.2 WHERE EMPNO = xxx_emp.empno;
            END IF;
        ELSIF xxx_emp.deptno = 20 THEN
            IF xxx_emp.sal * 1.4 < 3000 THEN
                UPDATE EMP SET SAL = xxx_emp.sal * 1.4 WHERE EMPNO = xxx_emp.empno;
            END IF;
        END IF;
    END LOOP;
    
    COMMIT;
    DBMS_OUTPUT.PUT_LINE('修改成功');
EXCEPTION
    WHEN others THEN
        DBMS_OUTPUT.PUT_LINE('SQLCODE=' || SQLCODE);
        DBMS_OUTPUT.PUT_LINE('SQLERRM=' || SQLERRM);
    ROLLBACK;
END;

※看起來沒什麼難度



※行級鎖定

就是再撈資料時,將table的行鎖起來,讓別人不能用,以免被人修改或刪除了,Hibernate把它變成悲觀鎖了

如:CURSOR xxx_cur IS SELECT * FROM EMP WHERE DEPTNO = 10 FOR UPDATE
.FOR UPDATE:鎖起來不讓別人用

.FOR UPDATE NOWAIT:因為有可能要用時已經被人鎖起來了,如果不加NOWAIT,就會一直等到釋放了才換自己做事;但加了就不等了,直接回來

.FOR UPDATE OF 欄位1, 欄位2...:在join很多表時,如果只使用FOR UPDATE不一定能更新成功,如果要確定能更新就必需要加上OF欄位名


DECLARE 
    CURSOR xxx_cur IS SELECT * FROM DEPT WHERE DEPTNO = 10 FOR UPDATE OF LOC;
BEGIN
    FOR xxx_dept IN xxx_cur LOOP
        UPDATE DEPT SET DNAME = 'xxx' WHERE CURRENT OF xxx_cur;
        -- DELETE FORM DEPT WHERE CURRENT OF xxx_cur;
    DBMS_OUTPUT.PUT_LINE(xxx_cur%ROWCOUNT);
    END LOOP;
    
    COMMIT;
    DBMS_OUTPUT.PUT_LINE('修改/刪除成功');
EXCEPTION
    WHEN others THEN
        DBMS_OUTPUT.PUT_LINE('SQLCODE=' || SQLCODE);
        DBMS_OUTPUT.PUT_LINE('SQLERRM=' || SQLERRM);
    ROLLBACK;
END;

※使用「CURRENT OF游標」時,一定要有FOR UPDATE,它的功能是定位目前的資料行



※動態游標

動態的指定內容,看第二個範例就懂了


※宣告動態游標

DECLARE
    TYPE xxx_dynamic IS REF CURSOR; -- RETURN DEPT%ROWTYPE;
    xxx xxx_dynamic;
    xxx_dept DEPT%ROWTYPE;
BEGIN
    OPEN xxx FOR SELECT * FROM DEPT;
    LOOP
        FETCH xxx INTO xxx_dept;
        EXIT WHEN xxx%NOTFOUND;
        DBMS_OUTPUT.PUT_LINE(xxx_dept.dname);
    END LOOP;
    CLOSE xxx;
END;

※因為FETCH是INTO到DEPT%ROWTYPE,所以註解可加可不加,不加的又叫「弱游標」



※弱游標的好處

DECLARE
    -- xxx SYS_REFCURSOR;
    TYPE xxx_dynamic IS REF CURSOR;
    xxx xxx_dynamic;
    
    xxx_dept DEPT%ROWTYPE;
    xxx_emp EMP%ROWTYPE;
BEGIN
    OPEN xxx FOR SELECT * FROM DEPT;
    LOOP
        FETCH xxx INTO xxx_dept;
        EXIT WHEN xxx%NOTFOUND;
        DBMS_OUTPUT.PUT_LINE(xxx_dept.dname);
    END LOOP;
    CLOSE xxx;
    
    DBMS_OUTPUT.PUT_LINE('');
    
    OPEN xxx FOR SELECT * FROM EMP WHERE DEPTNO = 10;
    LOOP
        FETCH xxx INTO xxx_emp;
        EXIT WHEN xxx%NOTFOUND;
        DBMS_OUTPUT.PUT_LINE(xxx_emp.ename);
    END LOOP;
    CLOSE xxx;
END;

※結果:
ACCOUNTING
RESEARCH
SALES
OPERATIONS

CLARK
KING
MILLER

※因為是弱游標,所以兩次的FETCH~INTO不一樣,但是還是OK的,所以才叫動態游標

※在Oracle9i提供了一個寫法來代替弱游標,就是註解那一行,只要SYS_REFCURSOR就可以取代TYPE和xxx dynamic那兩行了

游標一(隱式游標、顯式游標的查詢) (PL/SQL 九)

游標效能很低,所以要使用時要先想一下是否有必要使用,分成隱式游標(Implicit)和顯式游標(Explicit),可參考官網的 隱式游標 和 顯式游標


※隱式游標

就是不用打出「CURSOR」關鍵字,有四種,參考官網


※%ROWCOUNT

DECLARE 
    xxx_count NUMBER;
BEGIN
    SELECT COUNT(*) INTO xxx_count FROM EMP;
    DBMS_OUTPUT.PUT_LINE(SQL%ROWCOUNT);
    
    INSERT INTO DEPT VALUES(50, '業務部', '舊金山');
    DBMS_OUTPUT.PUT_LINE(SQL%ROWCOUNT);
    
    UPDATE DEPT SET DNAME='採購部' WHERE DEPTNO = 50;
    DBMS_OUTPUT.PUT_LINE(SQL%ROWCOUNT);
    
    DELETE FROM DEPT WHERE DEPTNO = 50;
    DBMS_OUTPUT.PUT_LINE(SQL%ROWCOUNT);
END;

※使用%ROWCOUNT可得知操作的行數,此例的結果是4個1



※%FOUND、%NOTFOUND

DECLARE 
BEGIN
    UPDATE EMP SET SAL = SAL * 1.2;
    
    IF SQL%FOUND THEN
        DBMS_OUTPUT.PUT_LINE('修改了' || SQL%ROWCOUNT || '筆');
    ELSE
        DBMS_OUTPUT.PUT_LINE('沒有資料被修改');
    END IF;
END;



※顯式游標的查詢

就是要明確的打出「CURSOR」關鍵字


※和LOOP、WHILE迴圈配合使用

DECLARE 
    CURSOR xxx_cur IS SELECT * FROM DEPT;
    xxx_dept DEPT%ROWTYPE;
BEGIN
    IF NOT xxx_cur%ISOPEN THEN
        OPEN xxx_cur;
    END IF;
    
    /*
    FETCH xxx_cur INTO xxx_dept;
    WHILE xxx_cur%FOUND LOOP
        DBMS_OUTPUT.PUT(xxx_cur%ROWCOUNT);
        DBMS_OUTPUT.PUT_LINE('=' || xxx_dept.dname);
        FETCH xxx_cur INTO xxx_dept;
    END LOOP;
    */
    
    LOOP
        FETCH xxx_cur INTO xxx_dept;
        EXIT WHEN xxx_cur%NOTFOUND;
        DBMS_OUTPUT.PUT(xxx_cur%ROWCOUNT);
        DBMS_OUTPUT.PUT_LINE('=' || xxx_dept.dname);
    END LOOP;
    
    CLOSE xxx_cur;
END;

※以上是兩種迴圈和游標的使用

※CURSOR xxx_cur IS SELECT * FROM DEPT 是省略寫法,全部是
CURSOR xxx_cur RETURN DEPT%ROWTYPE IS SELECT * FROM DEPT,也就是多了RETURN DEPT%ROWTYPE,因為查的是DEPT,所以預設也回傳DEPT的ROWTYPE,所以可以省略



※和FOR迴圈配合使用

DECLARE 
    CURSOR xxx_cur IS SELECT * FROM DEPT;
    xxx_dept DEPT%ROWTYPE;
BEGIN
    DBMS_OUTPUT.PUT_LINE('方法一');
    FOR xxx_dept IN xxx_cur LOOP
        DBMS_OUTPUT.PUT(xxx_cur%ROWCOUNT);
        DBMS_OUTPUT.PUT_LINE('=' || xxx_dept.dname);
    END LOOP;
    
    DBMS_OUTPUT.PUT_LINE(CHR(10) || '方法二');
    FOR xxx_dept IN (SELECT * FROM DEPT) LOOP
        DBMS_OUTPUT.PUT_LINE(xxx_dept.dname);
    END LOOP;
END;

※結果:
方法一
1=ACCOUNTING
2=RESEARCH
3=SALES
4=OPERATIONS

方法二
ACCOUNTING
RESEARCH
SALES
OPERATIONS

※和FOR迴圈配合時,不需要打開和關閉游標,也不用判斷哪時跳離迴圈,一切都交給系統控制,所以大部分的人都使用這一種

※方法一和方法二的差別就在方法二直接將SQL寫進去了,所以也無法使用%ROWCOUNT



※有參數的游標

DECLARE 
    CURSOR xxx_cur(ooo DEPT.DEPTNO%TYPE) IS SELECT * FROM DEPT WHERE DEPTNO = ooo;
BEGIN
    FOR xxx_dept IN xxx_cur(&pk) LOOP
        DBMS_OUTPUT.PUT(xxx_cur%ROWCOUNT || ' ');
        DBMS_OUTPUT.PUT(xxx_dept.deptno || ' ');
        DBMS_OUTPUT.PUT(xxx_dept.dname || ' ');
        DBMS_OUTPUT.PUT_LINE(xxx_dept.loc);
    END LOOP;
END;

※結果:
1 10 ACCOUNTING NEW YORK

※&pk為使用者輸入的參數值

 ※LOOP和WHILE要寫在OPEN xxx_cur(&pk)

※注意傳進去的參數不能用來判斷,如果是in的話,網路有用一種子查詢的方式,但我沒試出來,我用的是第26篇的動態SQL解決in的問題



※和陣列配合使用

DECLARE 
    CURSOR xxx_cur IS SELECT * FROM DEPT;
    TYPE xxx_index IS TABLE OF DEPT%ROWTYPE INDEX BY PLS_INTEGER;
    xxx xxx_index;
BEGIN
    FOR xxx_dept IN xxx_cur LOOP
        xxx(xxx_dept.deptno) := xxx_dept;
        DBMS_OUTPUT.PUT_LINE(xxx(xxx_dept.deptno).dname);
    END LOOP;
END;

※結果:
ACCOUNTING
RESEARCH
SALES
OPERATIONS



※和巢狀表配合使用

DECLARE 
    CURSOR xxx_cur IS SELECT * FROM DEPT;
    TYPE xxx_nested IS TABLE OF DEPT%ROWTYPE;
    xxx xxx_nested;
BEGIN
    IF NOT xxx_cur%ISOPEN THEN
        OPEN xxx_cur;
    END IF;
    
    FETCH xxx_cur BULK COLLECT INTO xxx;
    CLOSE xxx_cur;
    
    FOR i IN xxx.FIRST..xxx.LAST LOOP
        DBMS_OUTPUT.PUT_LINE(xxx(i).dname);
    END LOOP;
END;

※結果:
ACCOUNTING
RESEARCH
SALES
OPERATIONS

※使用BULK COLLECT一次取出,所以可以馬上關閉游標



※和VARRAY配合使用

DECLARE 
    CURSOR xxx_cur IS SELECT * FROM DEPT;
    TYPE xxx_varray IS VARRAY(3) OF DEPT%ROWTYPE;
    xxx xxx_varray;
BEGIN
    IF NOT xxx_cur%ISOPEN THEN
        OPEN xxx_cur;
    END IF;
    
    FETCH xxx_cur BULK COLLECT INTO xxx LIMIT 3;
    CLOSE xxx_cur;
    
    FOR i IN xxx.FIRST..xxx.LAST LOOP
        DBMS_OUTPUT.PUT_LINE(xxx(i).dname);
    END LOOP;
END;

※結果:
ACCOUNTING
RESEARCH
SALES

※BULK COLLECT INTO ~~~ LIMIT 可以限制資料量