PostgreSQL 觸發(fā)器函數(shù)

2021-09-03 17:36 更新
42.10.1. 數(shù)據(jù)改變的觸發(fā)器
42.10.2. 事件觸發(fā)器

PL/pgSQL可以被用來在數(shù)據(jù)更改或者數(shù)據(jù)庫事件上定義觸發(fā)器函數(shù)。觸發(fā)器函數(shù)用CREATE FUNCTION命令創(chuàng)建,它被聲明為一個沒有參數(shù)并且返回類型為trigger(對于數(shù)據(jù)更改觸發(fā)器)或者event_trigger(對于數(shù)據(jù)庫事件觸發(fā)器)的函數(shù)。名為PG_something 的特殊局部變量將被自動創(chuàng)建用以描述觸發(fā)該調(diào)用的條件。

42.10.1. 數(shù)據(jù)改變的觸發(fā)器

一個數(shù)據(jù)更改觸發(fā)器被聲明為一個沒有參數(shù)并且返回類型為trigger的函數(shù)。注意,如下所述,即便該函數(shù)準(zhǔn)備接收一些在CREATE TRIGGER中指定的參數(shù) — 這類參數(shù)通過TG_ARGV傳遞,也必須把它聲明為沒有參數(shù)。

當(dāng)一個PL/pgSQL函數(shù)當(dāng)做觸發(fā)器調(diào)用時,在頂層塊會自動創(chuàng)建一些特殊變量。它們是:

NEW

數(shù)據(jù)類型是RECORD;該變量為行級觸發(fā)器中的INSERT/UPDATE操作保持新數(shù)據(jù)行。在語句級別的觸發(fā)器以及DELETE操作,這個變量是null。

OLD

數(shù)據(jù)類型是RECORD;該變量為行級觸發(fā)器中的UPDATE/DELETE操作保持新數(shù)據(jù)行。在語句級別的觸發(fā)器以及INSERT操作,這個變量是null。

TG_NAME

數(shù)據(jù)類型是name;該變量包含實(shí)際觸發(fā)的觸發(fā)器名。

TG_WHEN

數(shù)據(jù)類型是text;是值為BEFOREAFTERINSTEAD OF的一個字符串,取決于觸發(fā)器的定義。

TG_LEVEL

數(shù)據(jù)類型是text;是值為ROWSTATEMENT的一個字符串,取決于觸發(fā)器的定義。

TG_OP

數(shù)據(jù)類型是text;是值為INSERTUPDATE、DELETETRUNCATE的一個字符串,它說明觸發(fā)器是為哪個操作引發(fā)。

TG_RELID

數(shù)據(jù)類型是oid;是導(dǎo)致觸發(fā)器調(diào)用的表的對象 ID。

TG_RELNAME

數(shù)據(jù)類型是name;是導(dǎo)致觸發(fā)器調(diào)用的表的名稱?,F(xiàn)在已經(jīng)被廢棄,并且可能在未來的一個發(fā)行中消失。使用TG_TABLE_NAME替代。

TG_TABLE_NAME

數(shù)據(jù)類型是name;是導(dǎo)致觸發(fā)器調(diào)用的表的名稱。

TG_TABLE_SCHEMA

數(shù)據(jù)類型是name;是導(dǎo)致觸發(fā)器調(diào)用的表所在的模式名。

TG_NARGS

數(shù)據(jù)類型是integer;在CREATE TRIGGER語句中給觸發(fā)器函數(shù)的參數(shù)數(shù)量。

TG_ARGV[]

數(shù)據(jù)類型是text數(shù)組;來自CREATE TRIGGER語句的參數(shù)。索引從 0 開始記數(shù)。非法索引(小于 0 或者大于等于tg_nargs)會導(dǎo)致返回一個空值。

一個觸發(fā)器函數(shù)必須返回NULL或者是一個與觸發(fā)器為之引發(fā)的表結(jié)構(gòu)完全相同的記錄/行值。

BEFORE引發(fā)的行級觸發(fā)器可以返回一個空來告訴觸發(fā)器管理器跳過對該行剩下的操作(即后續(xù)的觸發(fā)器將不再被引發(fā),并且不會對該行發(fā)生INSERT/UPDATE/DELETE)。如果返回了一個非空值,那么對該行值會繼續(xù)操作。返回不同于原始NEW的行值將修改將要被插入或更新的行。因此,如果該觸發(fā)器函數(shù)想要觸發(fā)動作正常成功而不修改行值, NEW(或者另一個相等的值)必須被返回。要修改將被存儲的行,可以直接在NEW中替換單一值并且返回修改后的NEW,或者構(gòu)建一個全新的記錄/行來返回。在一個DELETE上的前觸發(fā)器情況下,返回值沒有直接效果,但是它必須為非空以允許觸發(fā)器動作繼續(xù)下去。注意NEWDELETE觸發(fā)器中是空值,因此返回它通常沒有意義。在DELETE中的常用方法是返回OLD.

INSTEAD OF觸發(fā)器(總是行級觸發(fā)器,并且可能只被用于視圖)能夠返回空來表示它們沒有執(zhí)行任何更新,并且對該行剩余的操作可以被跳過(即后續(xù)的觸發(fā)器不會被引發(fā),并且該行不會被計入外圍INSERT/UPDATE/DELETE的行影響狀態(tài)中)。否則一個非空值應(yīng)該被返回用以表示該觸發(fā)器執(zhí)行了所請求的操作。對于 INSERTUPDATE操作,返回值應(yīng)該是NEW,觸發(fā)器函數(shù)可能對它進(jìn)行了修改來支持INSERT RETURNINGUPDATE RETURNING(這也將影響被傳遞給任何后續(xù)觸發(fā)器的行值,或者被傳遞給帶有ON CONFLICT DO UPDATEINSERT語句中一個特殊的EXCLUDED別名引用)。對于DELETE操作,返回值應(yīng)該是OLD。

一個AFTER行級觸發(fā)器或一個BEFOREAFTER語句級觸發(fā)器的返回值總是會被忽略,它可能也是空。不過,任何這些類型的觸發(fā)器可能仍會通過拋出一個錯誤來中止整個操作。

例 42.3展示了PL/pgSQL中一個觸發(fā)器函數(shù)的例子。

例 42.3. 一個 PL/pgSQL 觸發(fā)器函數(shù)

這個例子觸發(fā)器保證:任何時候一個行在表中被插入或更新時,當(dāng)前用戶名和時間也會被標(biāo)記在該行中。并且它會檢查給出了一個雇員的姓名以及薪水是一個正值。

CREATE TABLE emp (
    empname text,
    salary integer,
    last_date timestamp,
    last_user text
);

CREATE FUNCTION emp_stamp() RETURNS trigger AS $emp_stamp$
    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;
$emp_stamp$ LANGUAGE plpgsql;

CREATE TRIGGER emp_stamp BEFORE INSERT OR UPDATE ON emp
    FOR EACH ROW EXECUTE FUNCTION emp_stamp();

另一種記錄對表的改變的方法涉及到創(chuàng)建一個新表來為每一個發(fā)生的插入、更新或刪除保持一行。這種方法可以被認(rèn)為是對一個表的改變的審計。例 42.4展示了PL/pgSQL中一個審計觸發(fā)器函數(shù)的例子。

例 42.4. 一個用于審計的 PL/pgSQL 觸發(fā)器函數(shù)

這個例子觸發(fā)器保證了在emp表上的任何插入、更新或刪除一行的動作都被記錄(即審計)在emp_audit表中。當(dāng)前時間和用戶名會被記錄到行中,還有在其上執(zhí)行的操作類型。

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() RETURNS TRIGGER AS $emp_audit$
    BEGIN
        --
        -- 在 emp_audit 中創(chuàng)建一行來反映 emp 上執(zhí)行的動作,
        -- 使用特殊變量 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 觸發(fā)器,結(jié)果被忽略
    END;
$emp_audit$ LANGUAGE plpgsql;

CREATE TRIGGER emp_audit
AFTER INSERT OR UPDATE OR DELETE ON emp
    FOR EACH ROW EXECUTE FUNCTION process_emp_audit();

前一個例子的一種變體使用一個視圖將主表連接到審計表來展示每一項最后被修改是什么時間。這種方法還是記錄了對于表修改的完整審查跟蹤,但是也提供了審查跟蹤的一個簡化視圖,只為每一個項顯示從審查跟蹤生成的最后修改時間戳。例 42.5展示了在 PL/pgSQL中一個視圖上審計觸發(fā)器的例子。

例 42.5. 一個用于審計的 PL/pgSQL 視圖觸發(fā)器函數(shù)

這個例子在視圖上使用了一個觸發(fā)器讓它變得可更新,并且確保視圖中一行的任何插入、更新或刪除被記錄(即審計)在emp_audit表中。當(dāng)前時間和用戶名會被與執(zhí)行的操作類型一起記錄,并且該視圖會顯示每一行的最后修改時間。

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 OR REPLACE FUNCTION update_emp_view() RETURNS TRIGGER AS $$
    BEGIN
        --
        -- 執(zhí)行 emp 上所要求的操作,并且在 emp_audit 中創(chuàng)建一行來反映對 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;
$$ LANGUAGE plpgsql;

CREATE TRIGGER emp_audit
INSTEAD OF INSERT OR UPDATE OR DELETE ON emp_view
    FOR EACH ROW EXECUTE FUNCTION update_emp_view();

觸發(fā)器的一種用法是維護(hù)一個表的另一個匯總表。作為結(jié)果的匯總表可以用來在特定查詢中替代原始表 — 通常會大量減少運(yùn)行時間。這種技術(shù)常用于數(shù)據(jù)倉庫中,在其中被度量或被觀察數(shù)據(jù)的表(稱為事實(shí)表)可能會極度大。例 42.6展示了 PL/pgSQL中一個為數(shù)據(jù)倉庫事實(shí)表維護(hù)匯總表的觸發(fā)器函數(shù)的例子。

例 42.6. 一個 PL/pgSQL 用于維護(hù)匯總表的觸發(fā)器函數(shù)

這里詳述的模式有一部分是基于 Ralph Kimball 所作的The Data Warehouse Toolkit中的Grocery Store例子。

--
-- 主表 - 時間維度和銷售事實(shí)。
--
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 時修改匯總列的函數(shù)和觸發(fā)器。
--
CREATE OR REPLACE FUNCTION maint_sales_summary_bytime() RETURNS TRIGGER
AS $maint_sales_summary_bytime$
    DECLARE
        delta_time_key          integer;
        delta_amount_sold       numeric(15,2);
        delta_units_sold        numeric(12);
        delta_amount_cost       numeric(15,2);
    BEGIN

        -- 算出增量/減量數(shù)。
        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;
$maint_sales_summary_bytime$ LANGUAGE plpgsql;

CREATE TRIGGER maint_sales_summary_bytime
AFTER INSERT OR UPDATE OR DELETE ON sales_fact
    FOR EACH ROW EXECUTE FUNCTION maint_sales_summary_bytime();

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也可以利用傳遞表來觀察被觸發(fā)語句更改的整個行集合。CREATE TRIGGER命令會為一個或者兩個傳遞表分配名字,然后函數(shù)可以引用那些名字,就好像它們是只讀的臨時表一樣。例 42.7展示了一個例子。

例 42.7. 用傳遞表進(jìn)行審計

這個例子產(chǎn)生和例 42.4相同的結(jié)果,但并未使用一個為每一行都觸發(fā)的觸發(fā)器,而是在把相關(guān)信息收集到一個傳遞表中之后用了一個只為每個語句引發(fā)一次的觸發(fā)器。當(dāng)調(diào)用語句修改了很多行時,這種方法明顯比行觸發(fā)器方法快。注意我們必須為每一種事件建立一個單獨(dú)的觸發(fā)器聲明,因為每種情況的 REFERENCING子句必須不同。但是這并不能阻止我們使用單一的觸發(fā)器函數(shù)(實(shí)際上,使用三個單獨(dú)的函數(shù)會更好,因為可以避免在TG_OP上的運(yùn)行時測試)。

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() RETURNS TRIGGER AS $emp_audit$
    BEGIN
        --
        -- 在emp_audit中創(chuàng)建行來反映在emp上執(zhí)行的操作,
        -- 利用特殊變量TG_OP來區(qū)分操作。
        --
        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觸發(fā)器,所以結(jié)果被忽略
    END;
$emp_audit$ LANGUAGE plpgsql;

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();

42.10.2. 事件觸發(fā)器

PL/pgSQL可以被用來定義事件觸發(fā)器PostgreSQL要求一個可以作為事件觸發(fā)器調(diào)用的函數(shù)必須被聲明為沒有參數(shù)并且返回類型為event_trigger。

當(dāng)一個PL/pgSQL函數(shù)被作為一個事件觸發(fā)器調(diào)用,在頂層塊中會自動創(chuàng)建一些特殊變量。它們是:

TG_EVENT

數(shù)據(jù)類型是text;它是一個表示引發(fā)觸發(fā)器的事件的字符串。

TG_TAG

數(shù)據(jù)類型是text;它是一個變量,包含了該觸發(fā)器為之引發(fā)的命令標(biāo)簽。

例 42.8展示了PL/pgSQL中一個事件觸發(fā)器函數(shù)的例子。

例 42.8. 一個 PL/pgSQL 事件觸發(fā)器函數(shù)

這個例子觸發(fā)器在受支持命令每一次被執(zhí)行時會簡單地拋出一個NOTICE消息。

CREATE OR REPLACE FUNCTION snitch() RETURNS event_trigger AS $$
BEGIN
    RAISE NOTICE 'snitch: % %', tg_event, tg_tag;
END;
$$ LANGUAGE plpgsql;

CREATE EVENT TRIGGER snitch ON ddl_command_start EXECUTE FUNCTION snitch();


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號