本文介紹了觸發器函數的相關內容。
資料改變的觸發器
一個 PL/SQL 被聲明為一個沒有參數並且傳回型別為trigger的函數。注意,如下所述,即便該函數準備接收一些在CREATE TRIGGER中指定的參數 — 這類參數通過TG_ARGV傳遞,也必須把它聲明為沒有參數。
當一個PL/SQL函數當做觸發器調用時,在頂層塊會自動建立一些特殊變數。它們是:
NEW資料類型是RECORD;該變數為行級觸發器中的INSERT/UPDATE操作保持新資料行。在語句層級的觸發器以及DELETE操作,這個變數是 null。
OLD資料類型是RECORD;該變數為行級觸發器中的UPDATE/DELETE操作保持新資料行。在語句層級的觸發器以及INSERT操作,這個變數是 null。
TG_NAME資料類型是name;該變數包含實際觸發的觸發器名。
TG_WHEN資料類型是text;是值為BEFORE、AFTER或INSTEAD OF的一個字串,取決於觸發器的定義。
TG_LEVEL資料類型是text;是值為ROW或STATEMENT的一個字串,取決於觸發器的定義。
TG_OP資料類型是text;是值為INSERT、UPDATE、DELETE或TRUNCATE的一個字串,它說明觸發器是為哪個操作引發。
TG_RELID資料類型是oid;是導致觸發器調用的表的物件識別碼。
TG_RELNAME資料類型是name;是導致觸發器調用的表的名稱。現在已經被廢棄,並且可能在未來的一個發行中消失。使用TG_TABLE_NAME替代。
TG_TABLE_NAME資料類型是name;是導致觸發器調用的表的名稱。
TG_TABLE_SCHEMA資料類型是name;是導致觸發器調用的表所在的模式名。
TG_NARGS資料類型是integer;在CREATE TRIGGER語句中給觸發器函數的參數數量。
TG_ARGV[]資料類型是text數組;來自CREATE TRIGGER語句的參數。索引從 0 開始記數。非法索引(小於 0 或者大於等於tg_nargs)會導致返回一個空值。
一個觸發器函數必須返回NULL或者是一個與觸發器為之引發的表結構完全相同的記錄/行值。
BEFORE引發的行級觸發器可以返回一個空來告訴觸發器管理器跳過對該行剩下的操作(即後續的觸發器將不再被引發,並且不會對該行發生INSERT/UPDATE/DELETE)。如果返回了一個非空值,那麼對該行值會繼續操作。返回不同於原始NEW的行值將修改將要被插入或更新的行。因此,如果該觸發器函數想要觸發動作正常成功而不修改行值,NEW(或者另一個相等的值)必須被返回。要修改將被儲存的行,可以直接在NEW中替換單一值並且返回修改後的NEW,或者構建一個全新的記錄/行來返回。在一個DELETE上的前觸發器情況下,傳回值沒有直接效果,但是它必須為非空以允許觸發器動作繼續下去。注意NEW在DELETE觸發器中是空值,因此返回它通常沒有意義。在DELETE中的常用方法是返回OLD.
INSTEAD OF觸發器(總是行級觸發器,並且可能只被用於視圖)能夠返回空來表示它們沒有執行任何更新,並且對該行剩餘的操作可以被跳過(即後續的觸發器不會被引發,並且該行不會被計入外圍INSERT/UPDATE/DELETE的行影響狀態中)。否則一個非空值應該被返回用以表示該觸發器執行了所請求的操作。對於INSERT 和UPDATE操作,傳回值應該是NEW,觸發器函數可能對它進行了修改來支援INSERT RETURNING和UPDATE RETURNING(這也將影響被傳遞給任何後續觸發器的行值,或者被傳遞給帶有ON CONFLICT DO UPDATE的INSERT語句中一個特殊的EXCLUDED別名引用)。對於DELETE操作,傳回值應該是OLD。
一個AFTER行級觸發器或一個BEFORE或AFTER語句級觸發器的傳回值總是會被忽略,它可能也是空。不過,任何這些類型的觸發器可能仍會通過拋出一個錯誤來中止整個操作。
一個 PL/SQL 觸發器函數
這個例子觸發器保證:任何時候一個行在表中被插入或更新時,目前使用者名和時間也會被標記在該行中。並且它會檢查給出了一個僱員的姓名以及薪水是一個正值。
CREATE TABLE emp (
empname text,
salary integer,
last_date timestamp,
last_user text
);
CREATE TRIGGER emp_stamp BEFORE INSERT OR UPDATE ON emp
FOR EACH ROW
BEGIN
-- 檢查給出了 empname 以及 salary
IF NEW.empname IS NULL THEN
RAISE EXCEPTION 'empname cannot be null';
END IF;
IF NEW.salary IS NULL THEN
RAISE EXCEPTION '% cannot have null salary', NEW.empname;
END IF;
-- 誰會倒貼錢為我們工作?
IF NEW.salary < 0 THEN
RAISE EXCEPTION '% cannot have a negative salary', NEW.empname;
END IF;
-- 記住誰在什麼時候改變了工資單
NEW.last_date := current_timestamp;
NEW.last_user := current_user;
RETURN NEW;
END;另一種記錄對錶的改變的方法涉及到建立一個新表來為每一個發生的插入、更新或刪除保持一行。這種方法可以被認為是對一個表的改變的審計。
一個用於審計的 PL/SQL 觸發器函數
這個例子觸發器保證了在emp表上的任何插入、更新或刪除一行的動作都被記錄(即審計)在emp_audit表中。目前時間和使用者名稱會被記錄到行中,還有在其上執行的操作類型。
CREATE TABLE emp (
empname text NOT NULL,
salary integer
);
CREATE TABLE emp_audit(
operation char(1) NOT NULL,
stamp timestamp NOT NULL,
userid text NOT NULL,
empname text NOT NULL,
salary integer
);
CREATE TRIGGER emp_audit
AFTER INSERT OR UPDATE OR DELETE ON emp
FOR EACH ROW
BEGIN
--
-- 在 emp_audit 中建立一行來反映 emp 上執行的動作,
-- 使用特殊變數 TG_OP 來得到操作。
--
IF (TG_OP = 'DELETE') THEN
INSERT INTO emp_audit SELECT 'D', now(), user, OLD.*;
ELSIF (TG_OP = 'UPDATE') THEN
INSERT INTO emp_audit SELECT 'U', now(), user, NEW.*;
ELSIF (TG_OP = 'INSERT') THEN
INSERT INTO emp_audit SELECT 'I', now(), user, NEW.*;
END IF;
RETURN NULL; -- 因為這是一個 AFTER 觸發器,結果被忽略
END;前一個例子的一種變體使用一個視圖將主表串連到審計表來展示每一項最後被修改是什麼時間。這種方法還是記錄了對於表修改的完整審查跟蹤,但是也提供了審查跟蹤的一個簡化視圖,只為每一個項顯示從審查跟蹤產生的最後修改時間戳記。
一個用於審計的 PL/SQL 檢視觸發器函數
這個例子在視圖上使用了一個觸發器讓它變得可更新,並且確保視圖中一行的任何插入、更新或刪除被記錄(即審計)在emp_audit表中。目前時間和使用者名稱會被與執行的操作類型一起記錄,並且該視圖會顯示每一行的最後修改時間。
CREATE TABLE emp (
empname text PRIMARY KEY,
salary integer
);
CREATE TABLE emp_audit(
operation char(1) NOT NULL,
userid text NOT NULL,
empname text NOT NULL,
salary integer,
stamp timestamp NOT NULL
);
CREATE VIEW emp_view AS
SELECT e.empname,
e.salary,
max(ea.stamp) AS last_updated
FROM emp e
LEFT JOIN emp_audit ea ON ea.empname = e.empname
GROUP BY 1, 2;
CREATE TRIGGER emp_audit
INSTEAD OF INSERT OR UPDATE OR DELETE ON emp_view
FOR EACH ROW
BEGIN
--
-- 執行 emp 上所要求的操作,並且在 emp_audit 中建立一行來反映對 emp 的改變。
--
IF (TG_OP = 'DELETE') THEN
DELETE FROM emp WHERE empname = OLD.empname;
IF NOT FOUND THEN RETURN NULL; END IF;
OLD.last_updated = now();
INSERT INTO emp_audit VALUES('D', user, OLD.*);
RETURN OLD;
ELSIF (TG_OP = 'UPDATE') THEN
UPDATE emp SET salary = NEW.salary WHERE empname = OLD.empname;
IF NOT FOUND THEN RETURN NULL; END IF;
NEW.last_updated = now();
INSERT INTO emp_audit VALUES('U', user, NEW.*);
RETURN NEW;
ELSIF (TG_OP = 'INSERT') THEN
INSERT INTO emp VALUES(NEW.empname, NEW.salary);
NEW.last_updated = now();
INSERT INTO emp_audit VALUES('I', user, NEW.*);
RETURN NEW;
END IF;
END;觸發器的一種用法是維護一個表的另一個匯總表。作為結果的匯總表可以用來在特定查詢中替代原始表 — 通常會大量減少已耗用時間。這種技術常用於資料倉儲中,在其中被度量或被觀察資料的表(稱為事實表)可能會極度大。
一個 PL/SQL 用於維護匯總表的觸發器函數
這裡詳述的模式有一部分是基於 Ralph Kimball 所作的The Data Warehouse Toolkit中的Grocery Store例子。
-- 主表 - 時間維度和銷售事實。
--
CREATE TABLE time_dimension (
time_key integer NOT NULL,
day_of_week integer NOT NULL,
day_of_month integer NOT NULL,
month integer NOT NULL,
quarter integer NOT NULL,
year integer NOT NULL
);
CREATE UNIQUE INDEX time_dimension_key ON time_dimension(time_key);
CREATE TABLE sales_fact (
time_key integer NOT NULL,
product_key integer NOT NULL,
store_key integer NOT NULL,
amount_sold numeric(12,2) NOT NULL,
units_sold integer NOT NULL,
amount_cost numeric(12,2) NOT NULL
);
CREATE INDEX sales_fact_time ON sales_fact(time_key);
--
-- 匯總表 - 按時間匯總銷售
--
CREATE TABLE sales_summary_bytime (
time_key integer NOT NULL,
amount_sold numeric(15,2) NOT NULL,
units_sold numeric(12) NOT NULL,
amount_cost numeric(15,2) NOT NULL
);
CREATE UNIQUE INDEX sales_summary_bytime_key ON sales_summary_bytime(time_key);
--
-- 在 UPDATE、INSERT、DELETE 時修改匯總列的函數和觸發器。
--
CREATE TRIGGER maint_sales_summary_bytime
AFTER INSERT OR UPDATE OR DELETE ON sales_fact
FOR EACH ROW
DECLARE
delta_time_key integer;
delta_amount_sold numeric(15,2);
delta_units_sold numeric(12);
delta_amount_cost numeric(15,2);
BEGIN
-- 算出增量/減量數。
IF (TG_OP = 'DELETE') THEN
delta_time_key = OLD.time_key;
delta_amount_sold = -1 * OLD.amount_sold;
delta_units_sold = -1 * OLD.units_sold;
delta_amount_cost = -1 * OLD.amount_cost;
ELSIF (TG_OP = 'UPDATE') THEN
-- 禁止更改 the time_key 的更新-
-- (可能不會太麻煩,因為大部分的更改是用 DELETE + INSERT 完成的)。
IF ( OLD.time_key != NEW.time_key) THEN
RAISE EXCEPTION 'Update of time_key : % -> % not allowed',
OLD.time_key, NEW.time_key;
END IF;
delta_time_key = OLD.time_key;
delta_amount_sold = NEW.amount_sold - OLD.amount_sold;
delta_units_sold = NEW.units_sold - OLD.units_sold;
delta_amount_cost = NEW.amount_cost - OLD.amount_cost;
ELSIF (TG_OP = 'INSERT') THEN
delta_time_key = NEW.time_key;
delta_amount_sold = NEW.amount_sold;
delta_units_sold = NEW.units_sold;
delta_amount_cost = NEW.amount_cost;
END IF;
-- 插入或更新帶有新值的合計列。
<<insert_update>>
LOOP
UPDATE sales_summary_bytime
SET amount_sold = amount_sold + delta_amount_sold,
units_sold = units_sold + delta_units_sold,
amount_cost = amount_cost + delta_amount_cost
WHERE time_key = delta_time_key;
EXIT insert_update WHEN found;
BEGIN
INSERT INTO sales_summary_bytime (
time_key,
amount_sold,
units_sold,
amount_cost)
VALUES (
delta_time_key,
delta_amount_sold,
delta_units_sold,
delta_amount_cost
);
EXIT insert_update;
EXCEPTION
WHEN UNIQUE_VIOLATION THEN
-- 什麼也不做
END;
END LOOP insert_update;
RETURN NULL;
END;
INSERT INTO sales_fact VALUES(1,1,1,10,3,15);
INSERT INTO sales_fact VALUES(1,2,1,20,5,35);
INSERT INTO sales_fact VALUES(2,2,1,40,15,135);
INSERT INTO sales_fact VALUES(2,3,1,10,1,13);
SELECT * FROM sales_summary_bytime;
DELETE FROM sales_fact WHERE product_key = 1;
SELECT * FROM sales_summary_bytime;
UPDATE sales_fact SET units_sold = units_sold * 2;
SELECT * FROM sales_summary_bytime;AFTER也可以利用傳遞表來觀察被觸發語句更改的整個行集合。CREATE TRIGGER命令會為一個或者兩個傳遞表分配名字,然後函數可以引用那些名字,就好像它們是唯讀暫存資料表一樣。
用傳遞表進行審計。
這個例子產生和一個用於審計的 PL/SQL 觸發器函數相同的結果,但並未使用一個為每一行都觸發的觸發器,而是在把相關資訊收集到一個傳遞表中之後用了一個只為每個語句引發一次的觸發器。當調用語句修改了很多行時,這種方法明顯比行觸發器方法快。注意我們必須為每一種事件建立一個單獨的觸發器聲明,因為每種情況的REFERENCING子句必須不同。但是這並不能阻止我們使用單一的觸發器函數(實際上,使用三個單獨的函數會更好,因為可以避免在TG_OP上的運行時測試)。
CREATE TABLE emp (
empname text NOT NULL,
salary integer
);
CREATE TABLE emp_audit(
operation char(1) NOT NULL,
stamp timestamp NOT NULL,
userid text NOT NULL,
empname text NOT NULL,
salary integer
);
CREATE OR REPLACE FUNCTION process_emp_audit() RETURN TRIGGER IS
BEGIN
--
-- 在emp_audit中建立行來反映在emp上執行的操作,
-- 利用特殊變數TG_OP來區分操作。
--
IF (TG_OP = 'DELETE') THEN
INSERT INTO emp_audit
SELECT 'D', now(), user, o.* FROM old_table o;
ELSIF (TG_OP = 'UPDATE') THEN
INSERT INTO emp_audit
SELECT 'U', now(), user, n.* FROM new_table n;
ELSIF (TG_OP = 'INSERT') THEN
INSERT INTO emp_audit
SELECT 'I', now(), user, n.* FROM new_table n;
END IF;
RETURN NULL; -- 由於這是一個AFTER觸發器,所以結果被忽略
END;
CREATE TRIGGER emp_audit_ins
AFTER INSERT ON emp
REFERENCING NEW TABLE AS new_table
FOR EACH STATEMENT EXECUTE FUNCTION process_emp_audit();
CREATE TRIGGER emp_audit_upd
AFTER UPDATE ON emp
REFERENCING OLD TABLE AS old_table NEW TABLE AS new_table
FOR EACH STATEMENT EXECUTE FUNCTION process_emp_audit();
CREATE TRIGGER emp_audit_del
AFTER DELETE ON emp
REFERENCING OLD TABLE AS old_table
FOR EACH STATEMENT EXECUTE FUNCTION process_emp_audit();事件觸發程序
PL/SQL可以被用來定義 PL/SQL。本資料庫要求一個可以作為事件觸發程序調用的函數必須被聲明為沒有參數並且傳回型別為event_trigger。
當一個PL/SQL函數被作為一個事件觸發程序調用,在頂層塊中會自動建立一些特殊變數。它們是:
TG_EVENT資料類型是text;它是一個表示引發觸發器的事件的字串。
TG_TAG資料類型是text;它是一個變數,包含了該觸發器為之引發的命令標籤。
一個 PL/SQL 事件觸發程序函數展示了PL/SQL中一個事件觸發程序函數的例子。
一個 PL/SQL 事件觸發程序函數**
這個例子觸發器在受支援命令每一次被執行時會簡單地拋出一個NOTICE訊息。
CREATE OR REPLACE FUNCTION snitch() RETURN event_trigger IS
BEGIN
RAISE NOTICE 'snitch: % %', tg_event, tg_tag;
END;
CREATE EVENT TRIGGER snitch ON ddl_command_start EXECUTE FUNCTION snitch();