觸發器學習2
4,例一:行級觸發器之一
CREATE OR REPLACE TRIGGER salary_raiu
AFTER INSERT OR UPDATE OF amount ON salary
FOR EACH ROW
BEGIN
IF inserting THEN
dbms_output.put_line(‘插入’);
ELSIF updating THEN
dbms_output.put_line(‘更新amount列’);
END IF;
END;
以上是一個after insert和after update的行級觸發器。在第二行中of amount on salary的意思是隻有當amount列被更新時,update觸發器才會有效。所以,以下語句將不會執行觸發器:
Update salary set month = ‘200601’ where month = ‘200606’;
在觸發器主體的if語句表示式中,inserting, updating和deleting可以用來區分當前是在做哪一種DML操作,可以作為把多個類似觸發器合併在一個觸發器中判別觸發事件的屬性。
5,例二:行級觸發器之二
新建員工表employment
CREATE TABLE EMPLOYMENT
(
EMPLOYEE_ID NUMBER, –員工ID
MAXSALARY NUMBER –工資上限
)
插入兩條記錄
Insert into employment values(1, 1000);
Insert into employment values(2, 2000);
CREATE OR REPLACE TRIGGER salary_raiu
AFTER INSERT OR UPDATE OF amount ON salary
FOR EACH ROW
WHEN ( NEW.amount >= 1000 AND (old.amount IS NULL OR OLD.amount <= 500))
DECLARE
v_maxsalary NUMBER;
BEGIN
SELECT maxsalary
INTO v_maxsalary
FROM employment
WHERE employee_id = :NEW.employee_id;
IF :NEW.amount > v_maxsalary THEN
raise_application_error(-20000, ‘工資超限’);
END IF;
END;
以上的例子引入了一個新的表employment,表中的maxsalary欄位代表該員工每月所能分配的最高工資。下面的觸發器根據插入或修改記錄的employee_id,在employment表中查到該員工的每月最高工資,如果插入或修改後的amount超過這個值,則報錯誤。
程式碼中的when子句表明了該觸發器只針對修改或插入後的amount值超過1000,而修改前的amount值小於500的記錄。New物件和old物件分別表示了操作前和操作後的記錄物件。對於insert操作,由於當前操作記錄無歷史物件,所以old物件中所有屬性是null;對於delete操作,由於當前操作記錄沒有更新物件,所以new物件中所有屬性也是null。但在這兩種情況下,並不影響old和new物件的引用和在觸發器主體中的使用,和普通的空值作同樣的處理。
在觸發器主體中,先通過:new.employee_id,得到該員工的工資上限,然後在if語句中判斷更新後的員工工資是否超限,如果超限則錯誤程式碼為-20000,錯誤資訊為“工資超限”的自定義錯誤。其中的raise_application_error包含兩個引數,前一個是自定義錯誤程式碼,後一個是自定義錯誤程式碼資訊。其中自定義錯誤程式碼必須小於或等於-20000。執行完該語句後,一個異常被丟擲,如果在上一層有exception子句,該異常將被捕獲。如下面程式碼:
DECLARE
code NUMBER;
msg VARCHAR2(500);
BEGIN
INSERT INTO salary (employee_id, amount) VALUES (2, 5000);
EXCEPTION
WHEN OTHERS THEN
code := SQLCODE;
msg := substr(SQLERRM, 1, 500);
dbms_output.put_line(code);
dbms_output.put_line(msg);
END;
執行後,將在output中或者sqlplus視窗中見著以下資訊:
-20000
ORA-20000: 工資超出限制
ORA-06512: 在”SCOTT.SALARY_RAI”, line 9
ORA-04088: 觸發器 ‘SCOTT.SALARY_RAI’ 執行過程中出錯
這裡的raise_application_error相當於拒絕了插入或者修改事務,當上層程式碼接受到這個異常後,判斷該異常程式碼等於-20000,可以作出回滾事務或者繼續其他事務的處理。
以上兩個例子中用到的inserting, updating, deleting和raise_application_error都是dbms_standard包中的函式,具體的說明可以參照Oracle的幫助文件。
create or replace package sys.dbms_standard is
procedure raise_application_error(num binary_integer, msg varchar2,
function inserting return boolean;
function deleting return boolean;
function updating return boolean;
function updating (colnam varchar2) return boolean;
end;
對於before和after行級觸發器,:new和:old物件的屬性值都是一樣的,主要是對於在Oracle約束(Constraint)之前或之後的執行觸發器的選擇。需要注意的是,可以在before行觸發器中更改:new物件中的值,但是在after行觸發器就不行。
下面介紹一種instead of觸發器,該觸發器主要使用在對檢視的更新上,以下是instead of觸發器的語法:
CREATE OR REPLACE TRIGGER trigger_name
INSTEAD OF <insert | update | delete> ON view_name
[FOR EACH ROW]
WHEN (condition)
DECLARE
BEGIN
--觸發器程式碼
END;
其他部分語法
同前面所述的before和after語法是一樣的,唯一不同的是在第二行用上了instead of關鍵字。對於普通的檢視來說,進行insert等操作是被禁止的,因為Oracle無法知道操作的欄位具體是哪個表中的欄位。但我們可以通過建立instead of觸發器,在觸發器主體中告訴Oracle應該更新,刪除或者修改哪些表的哪部分欄位。如:
6,例三:instead of觸發器
新建檢視
CREATE VIEW employee_salary(employee_id, maxsalary, MONTH, amount) AS
SELECT a.employee_id, a.maxsalary, b.MONTH, b.amount
FROM employment a, salary b
WHERE a.employee_id = b.employee_id
如果執行插入語句
INSERT INTO employee_salary(employee_id, maxsalary, MONTH, amount)
VALUES(10, 100000, ‘200606’, 10000);
系統會報錯:
ORA-01779:無法修改與非鍵值儲存表對應的列
我們可以通過建立以下的instead of儲存過程,將插入檢視的值分別插入到兩個表中:
create or replace trigger employee_salary_rii
instead of insert on employee_salary
for each ROW
DECLARE
v_cnt NUMBER;
BEGIN
–檢查是否存在該員工資訊
SELECT COUNT(*)
INTO v_cnt
FROM employment
WHERE employee_id = :NEW.employee_id;
IF v_cnt = 0 THEN
INSERT INTO employment
(employee_id, maxsalary)
VALUES
(:NEW.employee_id, :NEW.maxsalary);
END IF;
–檢查是否存在該員工的工資資訊
SELECT COUNT(*)
INTO v_cnt
FROM salary
WHERE employee_id = :NEW.employee_id
AND MONTH = :NEW.MONTH;
IF v_cnt = 0 THEN
INSERT INTO salary
(employee_id, MONTH, amount)
VALUES
(:NEW.employee_id, :NEW.MONTH, :NEW.amount);
END IF;
END employee_salary_rii;
該觸發器被建立後,執行上述insert操作,系統就會提示成功插入一條記錄。
但需要注意的是,這裡的“成功插入一條記錄”,只是Oracle並未發現觸發器中有異常丟擲,而根據insert語句中涉及的記錄數作出一個判斷。若觸發器的主體什麼都沒有,只是一個空語句,Oracle也會報“成功插入一條記錄”。同樣道理,即使在觸發器主體裡往多個表中插入十條記錄,Oracle的返回也是“成功插入一條記錄”。
行級觸發器可以解決大部分的問題,但是如果需要對本表進行掃描檢查,比如要檢查總的工資是否超限了,用行級觸發器是不行的,因為行級觸發器主體中不能有涉及到關聯表的事務,這時就需要用到語句級觸發器。以下是語句級觸發器的語法:
CREATE OR REPLACE TRIGGER trigger_name
<before | after | instead of ><insert | update | delete > ON table_name
DECLARE
BEGIN
--觸發器主體
END;
從語法定義上來看,行級觸發器少了for each row,也不能使用when子句來限定入口條件,其他部分都是一樣的,包括insert, update, delete和instead of都可以使用。
7,例四:語句級觸發器之一
CREATE OR REPLACE TRIGGER salary_saiu
AFTER INSERT OR UPDATE OF amount ON salary
DECLARE
v_sumsalary NUMBER;
BEGIN
SELECT SUM(amount) INTO v_sumsalary FROM salary;
IF v_sumsalary > 500000 THEN
raise_application_error(-20001, ‘總工資超過500000’);
END IF;
END;
以上程式碼定義了一個語句級觸發器,該觸發器檢查在insert和update了amount欄位後操作後,工資表中所有工資記錄累加起來是否超過500000,如果超過則丟擲異常。從這個例子可以看出,語句級觸發器可以對關聯表表進行掃描,掃描得到的結果可以用來作為判斷一致性的標誌。需要注意的是,在before語句觸發器主體和after語句觸發器主體中對關聯表進行掃描,結果是不一樣的。在before語句觸發器主體中掃描,掃描結果將不包括新插入和更新的記錄,也就是說當以上程式碼換成 before觸發器後,以下語句將不報錯:
INSERT INTO salary(employee_id, month, amount) VALUEs(2, ‘200601’, 600000)
這是因為在主體中得到的v_sumsalary並不包括新插入的600000工資。
另外,在語句級觸發器中不能使用:new和:old物件,這一點和行級觸發器是顯著不同的。如果需要檢查插入或更新後的記錄,可以採用臨時表技術。
臨時表是一種Oracle資料庫物件,其特點是當建立資料的程序結束後,程序所建立的資料也隨之清除。程序與程序不可以互相訪問同一臨時表中對方的資料,而且對臨時表進行操作也不產生undo日誌,減少了資料庫的消耗。具體有關臨時表的知識,可以參看有關書籍。
為了在語句級觸發器中訪問新插入後修改後的記錄,可以增加行級觸發器,將更新的記錄插入臨時表中,然後在語句級觸發器中掃描臨時表,獲得修改後的記錄。臨時表的表結構一般與關聯表的結構一致。
8,例五:語句級觸發器之二
目的:限制每個員工的總工資不能超過50000,否則停止對該表操作。
建立臨時表
create global temporary table SALARY_TMP
(
EMPLOYEE_ID NUMBER,
MONTH VARCHAR2(6),
AMOUNT NUMBER
)
on commit delete rows;
為了把操作記錄插入到臨時表中,建立行級觸發器:
CREATE OR REPLACE TRIGGER salary_raiu
AFTER INSERT OR UPDATE OF amount ON salary
FOR EACH ROW
BEGIN
INSERT INTO salary_tmp(employee_id, month, amount)
VALUES(:NEW.employee_id, :NEW.MONTH, :NEW.amount);
END;
該觸發器的作用是把更新後的記錄資訊插入到臨時表中,如果更新了多條記錄,則每條記錄都會儲存在臨時表中。
建立語句級觸發器:
CREATE OR REPLACE TRIGGER salary_sai
AFTER INSERT OR UPDATE OF amount ON salary
DECLARE
v_sumsalary NUMBER;
BEGIN
FOR cur IN (SELECT * FROM salary_tmp) LOOP
SELECT SUM(amount)
INTO v_sumsalary
FROM salary
WHERE employee_id = cur.employee_id;
IF v_sumsalary > 50000 THEN
raise_application_error(-20002, ‘員工累計工資超過50000’);
END IF;
DELETE FROM salary_tmp;
END LOOP;
END;
該觸發器首先用遊標從salary_tmp臨時表中逐條讀取更新或插入的記錄,取employee_id,在關聯表salary中查詢所有相同員工的工資記錄,並求和。若某員工工資總和超過50000,則丟擲異常。如果檢查通過,則清空臨時表,避免下次檢查相同的記錄。
執行以下語句:
INSERT INTO salary(employee_id, month, amount) VALUEs(7, ‘200601’, 20000);
INSERT INTO salary(employee_id, month, amount) VALUEs(7, ‘200602’, 20000);
INSERT INTO salary(employee_id, month, amount) VALUEs(7, ‘200603’, 20000);
在執行第三句時系統報錯:
ORA-20002:員工累計工資超過50000
查詢salary表,發現前兩條記錄正常插入了,第三條記錄沒有插入。
如果系統結構比較複雜,而且觸發器的程式碼比較多,在觸發器主體中寫過多的程式碼,對於維護來說是一個困難。這時可以將所有觸發器的程式碼寫到同一個包中,不同的觸發器程式碼以不同的儲存過程封裝,然後觸發器主體中呼叫這部分程式碼。
9,例六:用包封裝觸發器程式碼
目的:改寫例五,封裝觸發器主體程式碼
建立程式碼包:
CREATE OR REPLACE PACKAGE BODY salary_trigger_pck IS
PROCEDURE load_salary_tmp(i_employee_id IN NUMBER,
i_month IN VARCHAR2,
i_amount IN NUMBER) IS
BEGIN
INSERT INTO salary_tmp VALUES (i_employee_id, i_month, i_amount);
END load_salary_tmp;
PROCEDURE check_salary IS
v_sumsalary NUMBER;
BEGIN
FOR cur IN (SELECT * FROM salary_tmp) LOOP
SELECT SUM(amount)
INTO v_sumsalary
FROM salary
WHERE employee_id = cur.employee_id;
IF v_sumsalary > 50000 THEN
raise_application_error(-20002, ‘員工累計工資超過50000’);
END IF;
DELETE FROM salary_tmp;
END LOOP;
END check_salary;
END salary_trigger_pck;
包salary_trigger_pck中有兩個儲存過程,load_salary_tmp用於在行級觸發器中呼叫,往salary_tmp臨時表中裝載更新或插入記錄。而check_salary用於在語句級觸發器中檢查員工累計工資是否超限。
修改行級觸發器和語句級觸發器:
CREATE OR REPLACE TRIGGER salary_raiu
AFTER INSERT OR UPDATE OF amount ON salary
FOR EACH ROW
BEGIN
salary_trigger_pck.load_salary_tmp(:NEW.employee_id, :NEW.MONTH, :NEW.amount);
END;
CREATE OR REPLACE TRIGGER salary_sai
AFTER INSERT OR UPDATE OF amount ON salary
BEGIN
salary_trigger_pck.check_salary;
END;
這樣主要程式碼就集中到了salary_trigger_pck中,觸發器主體中只實現了一個呼叫功能。
10,觸發器命名規範
為了方便對觸發器命名和根據觸發器名稱瞭解觸發器含義,需要定義觸發器的命名規範:
Trigger_name = table_name_trg_<R|S><A|B|I><I|U|D>
觸發器名限於30個字元。必須縮寫表名,以便附加觸發器屬性資訊。
<R|S>
基於行級(row)還是語句級(statement)的觸發器
<A|B|I>
after, before或者是instead of觸發器
<I|U|D>
觸發事件是insert,update還是delete。如果有多個觸發事件則連著寫
例如:
Salary_rai salary表的行級after觸發器,觸發事件是insert
Employee_sbiud employee表的語句級before觸發器,觸發事件是insert,update和delete