# 43.10.触发功能

43.10.1. 触发数据更改

43.10.2. 触发事件

PL/pgSQL可用于定义数据更改或数据库事件的触发函数。使用创建函数命令,将其声明为不带参数的函数,返回类型为触发(对于数据更改触发器)或事件触发(用于数据库事件触发器)。名为甘油三酯_*某物*是自动定义的,用于描述触发呼叫的条件。

# 43.10.1.触发数据更改

A.数据更改触发器声明为不带参数的函数,返回类型为触发。请注意,即使函数希望接收中指定的某些参数,也必须声明该函数,且不带任何参数创建触发器-这样的论点是通过TG_ARGV,如下所述。

当调用PL/pgSQL函数作为触发器时,会在顶级块中自动创建几个特殊变量。他们是:

数据类型记录; 用于保存新数据库行的变量插入/使现代化行级触发器中的操作。此变量在语句级触发器中为null,对于删去操作。

古老的

数据类型记录; 用于保存旧数据库行的变量使现代化/删去行级触发器中的操作。此变量在语句级触发器中为null,对于插入操作。

TG_名称

数据类型名称; 包含实际触发的触发器名称的变量。

什么时候

数据类型文本; 一串之前, 之后而不是,这取决于触发器的定义。

TG_水平

数据类型文本; 一串一行陈述取决于触发器的定义。

TG_OP

数据类型文本; 一串插入, 使现代化, 删去截断告诉他是哪一个操作触发的。

热释光

数据类型老年人; 导致触发器调用的表的对象ID。

TG_RELNAME

数据类型名称; 导致触发器调用的表的名称。这一点现在已被弃用,并可能在未来的版本中消失。使用TG_表_名称相反

TG_表_名称

数据类型名称; 导致触发器调用的表的名称。

TG_表_模式

数据类型名称; 导致触发器调用的表的架构的名称。

特古纳格斯

数据类型整数; 中指定给触发器函数的参数数创建触发器陈述

TG_ARGV[]

数据类型数组文本; 来自创建触发器陈述索引从0开始计数。无效索引(小于0或大于或等于特古纳格斯)结果为空值。

触发器函数必须返回无效的或者一个记录/行值,该值正好与触发触发器的表的结构相同。

行级触发器被触发之前可以返回null,以指示触发器管理器跳过此行的其余操作(即,后续触发器不会触发,并且插入/使现代化/删去不适用于此行)。如果返回非空值,则操作将继续该行值。返回的行值与更改将插入或更新的行。因此,如果触发函数希望触发操作正常成功,而不改变行值,(或与之相等的值)必须返回。要更改要存储的行,可以直接在中替换单个值并返回修改后的,或创建一个完整的新记录/行以返回。在触发前打开的情况下删去,返回的值没有直接影响,但必须为非null才能继续执行触发器操作。注意在中为空删去触发器,所以返回通常是不明智的。英语中常用的成语删去触发器是返回的古老的.

而不是触发器(总是行级别的触发器,只能在视图上使用)可以返回null,表示它们没有执行任何更新,并且应该跳过该行的其余操作(即,不触发后续触发器,并且该行不计入周围受影响的行状态)插入/使现代化/删去)。否则,应返回非空值,以表示触发器执行了请求的操作。对于插入使现代化操作时,返回值应为,触发器功能可能会对其进行修改以支持插入返回更新返回(这也会影响传递给任何后续触发器或传递给特殊触发器的行值。)排除内部的别名引用插入带有关于冲突,请更新条款)。对于删去操作时,返回值应为古老的.

触发的行级触发器的返回值之后或者触发语句级触发器之前之后总是被忽视;它也可能是空的。但是,这些类型的触发器中的任何一种都可能会引发错误,从而中止整个操作。

例43.3显示了PL/pgSQL中触发器函数的示例。

例43.3.PL/pgSQL触发器函数

此示例触发器确保在表中插入或更新行时,当前用户名和时间都会压印到该行中。它会检查是否给出了员工的姓名,以及工资是否为正值。

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

CREATE FUNCTION emp_stamp() RETURNS trigger AS $emp_stamp$
    BEGIN
        -- Check that empname and salary are given
        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;

        -- Who works for us when they must pay for it?
        IF NEW.salary < 0 THEN
            RAISE EXCEPTION '% cannot have a negative salary', NEW.empname;
        END IF;

        -- Remember who changed the payroll when
        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();

记录表更改的另一种方法是创建一个新表,为每次插入、更新或删除保存一行。这种方法可以被认为是审核表的更改。例43.4显示了PL/pgSQL中审计触发器函数的示例。

例43.4.用于审计的PL/pgSQL触发器函数

此示例触发器确保电磁脉冲表格记录(即审计)在emp_审计桌子当前时间和用户名以及在该行上执行的操作类型将被压印到该行中。

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
        --
        -- Create a row in emp_audit to reflect the operation performed on emp,
        -- making use of the special variable TG_OP to work out the operation.
        --
        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; -- result is ignored since this is an AFTER trigger
    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();

上一个示例的一个变体使用一个视图将主表连接到审计表,以显示每个条目上次修改的时间。这种方法仍然记录表中更改的完整审计跟踪,但也提供了审计跟踪的简化视图,只显示从每个条目的审计跟踪中导出的最后修改的时间戳。例43.5显示了PL/pgSQL中视图上的审核触发器的示例。

例43.5.用于审核的PL/pgSQL视图触发器函数

本例使用视图上的触发器使其可更新,并确保在emp_审计桌子记录当前时间和用户名,以及执行的操作类型,视图显示每行的上次修改时间。

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
        --
        -- Perform the required operation on emp, and create a row in emp_audit
        -- to reflect the change made to 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();

触发器的一个用途是维护另一个表的摘要表。对于某些查询,生成的摘要可以代替原始表使用——通常运行时间大大缩短。这种技术通常用于数据仓库,其中测量或观察到的数据表(称为事实表)可能非常大。例43.6显示了PL/pgSQL中的触发器函数的示例,该函数为数据仓库中的事实表维护摘要表。

例43.6.用于维护汇总表的PL/pgSQL触发器函数

这里详细介绍的模式部分基于杂货店来自数据仓库工具箱拉尔夫·金博尔著。

--
-- Main tables - time dimension and sales fact.
--
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);

--
-- Summary table - sales by time.
--
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);

--
-- Function and trigger to amend summarized column(s) on UPDATE, INSERT, DELETE.
--
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

        -- Work out the increment/decrement amount(s).
        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

            -- forbid updates that change the time_key -
            -- (probably not too onerous, as DELETE + INSERT is how most
            -- changes will be made).
            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 or update the summary row with the new values.
        <<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
                    -- do nothing
            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;

之后触发器也可以利用过渡表检查由触发语句更改的整组行。这个创建触发器命令为一个或两个转换表指定名称,然后函数可以引用这些名称,就像它们是只读临时表一样。例43.7下面是一个例子。

例43.7.使用转换表进行审计

本例产生的结果与例43.4,但它不使用为每一行触发的触发器,而是在收集转换表中的相关信息后,为每个语句触发一次触发器。当调用语句修改了许多行时,这可能比行触发器方法快得多。请注意,我们必须为每种类型的事件单独声明触发器,因为引用每种情况的条款必须不同。但如果我们愿意,这并不能阻止我们使用单一触发功能。(在实践中,最好使用三个单独的函数,并避免对其进行运行时测试。)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() RETURNS TRIGGER AS $emp_audit$
    BEGIN
        --
        -- Create rows in emp_audit to reflect the operations performed on emp,
        -- making use of the special variable TG_OP to work out the operation.
        --
        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; -- result is ignored since this is an AFTER trigger
    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();

# 43.10.2.触发事件

PL/pgSQL可用于定义事件触发器.PostgreSQL要求作为事件触发器调用的函数必须声明为不带参数且返回类型为的函数事件触发.

当调用PL/pgSQL函数作为事件触发器时,会在顶级块中自动创建几个特殊变量。他们是:

TG_事件

数据类型文本; 表示触发事件的字符串。

TG_标签

数据类型文本; 变量,该变量包含为其触发触发器的命令标记。

例43.8显示了PL/pgSQL中事件触发器函数的示例。

例43.8.PL/pgSQL事件触发器函数

这个示例触发器只会引发一个注意每次执行受支持的命令时显示消息。

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