39.9. 触发器函数 #
PL/pgSQL 可以被用来定义触发器函数。触发器函数用 CREATE FUNCTION 命令创建,它被声明为一个没有参数并且返回类型为
trigger 的函数。注意,即便该函数准备接收一些在
CREATE TRIGGER 中指定的参数 —
这类参数通过 TG_ARGV 传递(如下所述),也必须把它声明为没有参数。
当一个 PL/pgSQL 函数作为触发器被调用时,会在顶层块中自动创建一些特殊变量。它们是:
NEW数据类型为
RECORD;该变量保存行级触发器中用于INSERT/UPDATE操作的新数据行。在语句级触发器和DELETE操作中,该变量为NULL。OLD数据类型为
RECORD;该变量保存行级触发器中用于UPDATE/DELETE操作的旧数据行。在语句级触发器和INSERT操作中,该变量为NULL。TG_NAME数据类型为
name;该变量包含实际触发的触发器的名称。TG_WHEN数据类型为
text;根据触发器定义,其值为字符串BEFORE或AFTER。TG_LEVEL数据类型为
text;根据触发器定义,其值为字符串ROW或STATEMENT。TG_OP数据类型为
text;表示触发器对应操作的字符串:INSERT、UPDATE、DELETE或TRUNCATE。TG_RELID数据类型为
oid;导致触发器调用的表的对象 ID。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 行级触发器可以返回 null,以通知触发器管理器跳过该行后续的操作(也就是说,不再触发后续触发器,并且不会对该行执行
INSERT/UPDATE/DELETE)。如果返回非 null 值,则操作会继续进行,并使用该行值。返回一个不同于原始 NEW 的行值会改变即将插入或更新的行。因此,如果触发器函数希望触发动作正常成功而不修改行值,就必须返回 NEW(或与之相等的值)。若要修改将被存储的行,可以直接替换 NEW 中的单个值并返回修改后的 NEW,或者构造一个完整的新记录/行来返回。对于作用于
DELETE 的 before 触发器,返回值本身没有直接效果,但必须为非 null 才能让触发器动作继续。注意,在
DELETE 触发器中
NEW 为 null,因此通常没有理由返回它。在
DELETE 触发器中,常见写法是返回
OLD。
一个 AFTER 行级触发器或一个
BEFORE 或
AFTER 语句级触发器的返回值总是会被忽略,因此也可以返回 null。不过,任何这些类型的触发器可能仍会通过抛出一个错误来中止整个操作。
例 39.3展示了 PL/pgSQL 中一个触发器函数的示例。
例 39.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 she 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 PROCEDURE emp_stamp();另一种记录对表的改变的方法涉及到创建一个新表来为每一个发生的插入、更新或删除保持一行。这种方法可以被认为是对一个表的改变的审计。例 39.4展示了 PL/pgSQL 中一个审计触发器函数的示例。
例 39.4. 一个用于审计的 PL/pgSQL 触发器函数
这个示例触发器保证
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 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,
-- make 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.*;
RETURN OLD;
ELSIF (TG_OP = 'UPDATE') THEN
INSERT INTO emp_audit SELECT 'U', now(), user, NEW.*;
RETURN NEW;
ELSIF (TG_OP = 'INSERT') THEN
INSERT INTO emp_audit SELECT 'I', now(), user, NEW.*;
RETURN 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 PROCEDURE process_emp_audit();触发器的一种用法是维护另一个表的汇总表。所得的汇总可以替代原表用于某些查询 — 运行时间往往大幅缩短。这种技术常用于数据仓库,其中被测量或观察数据(称为事实表)的表可能极其庞大。例 39.5展示了 PL/pgSQL 中一个触发器函数的示例,它为数据仓库中的一个事实表维护汇总表。
例 39.5. 一个维护汇总表的 PL/pgSQL 触发器函数
这里详述的模式部分基于 Ralph Kimball 的 The Data Warehouse Toolkit 中的 Grocery Store 示例。
--
-- 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 PROCEDURE 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;