《PostgreSQL 開發指南》第 27 篇 觸發器

上一篇我們介紹瞭如何在 PostgreSQL 中利用 PL/pgSQL 過程語言實現存儲過程和自定義函數。PostgreSQL 自定義函數還可以用於實現另一種功能:觸發器。

觸發器概述

PostgreSQL 觸發器(trigger)是一種特殊的函數,當某個數據變更事件(INSERT、UPDATE、DELETE 或者 TRUNCATE)或者數據庫事件(DDL 語句)發生時自動執行,而不是由用戶或者應用程序進行調用。

基於某個表或者視圖數據變更的觸發器被稱爲數據變更觸發器(DML 觸發器),基於數據庫事件的觸發器被稱爲事件觸發器(DDL 觸發器)。一般我們更多使用的是數據變更觸發器。

trigger

對於數據變更觸發器,PostgreSQL 支持兩種級別的觸發方式:行級(row-level)觸發器語句級(statement-level)觸發器。這兩者的區別在於觸發的時機和觸發次數。例如,對於一個影響 20 行數據的 UPDATE 語句,行級觸發器將會觸發器 20 次,而語句級觸發器只會觸發 1 次。

觸發器可以在事件發生之前(BEFORE)或者之後(AFTER)觸發。如果在事件之前觸發,它可以跳過針對當前行的修改,甚至修改被更新或插入的數據;如果在事件之後觸發,觸發器可以獲得所有的變更結果。INSTEAD OF 觸發器可以用於替換數據變更的操作,只能基於視圖定義。

下表列出了 PostgreSQL 中支持的各種觸發器:

觸發時機 觸發事件 行級觸發器 語句級觸發器
BEFORE INSERT、UPDATE、DELETE 表和外部表 表、視圖和外部表
BEFORE TRUNCATE --
AFTER INSERT、UPDATE、DELETE 表和外部表 表、視圖和外部表
AFTER TRUNCATE --
INSTEAD OF INSERT、UPDATE、DELETE 視圖 --
INSTEAD OF TRUNCATE -- --

觸發器對於多應用共享的數據庫而言非常有用,可以將跨應用的功能存儲在數據庫中,當表中的數據發生任何變化時都會自動執行觸發器的操作。例如,可以用觸發器實現數據修改的歷史審計,而不需要各種應用程序實現任何相關的邏輯。

另外,觸發器還可以用於實現複雜的數據完整性和業務規則。例如,在非業務時間不允許修改用戶的信息。

但是另一方面,觸發器可能帶來的問題就是在不清楚它們的存在和邏輯時可能會影響數據修改的結果和性能。

創建觸發器

PostgreSQL 觸發器的創建分爲兩步:

  1. 使用 CREATE FUNCTION 語句創建一個觸發器函數;
  2. 使用 CREATE TRIGGER 語句將該函數與表進行關聯。

首先,創建一個觸發器函數:

CREATE [ OR REPLACE ] FUNCTION trigger_function ()
  RETURNS trigger
AS $$
DECLARE
  declarations
BEGIN
  statements;
  ...
END; $$
LANGUAGE plpgsql;

觸發器函數與普通函數的區別在於它沒有參數,並且返回類型爲 trigger;觸發器函數也可以使用其他過程語言,本文只涉及 PL/pgSQL。在觸發器函數內部,系統自動創建了許多特殊的變量:

  • NEW ,類型爲 RECORD,代表了行級觸發器 INSERT、UPDATE 操作之後的新數據行。對於 DELETE 操作或者語句級觸發器而言,該變量爲 null;
  • OLD,類型爲 RECORD,代表了行級觸發器 UPDATE、DELETE 操作之前的舊數據行。對於 INSERT 操作或者語句級觸發器而言,該變量爲 null;
  • TG_NAME,觸發器的名稱;
  • TG_WHEN,觸發的時機,例如 BEFORE、AFTER 或者 INSTEAD OF;
  • TG_LEVEL,觸發器的級別,ROW 或者 STATEMENT;
  • TG_OP,觸發的操作,INSERT、UPDATE、DELETE 或者 TRUNCATE;
  • TG_RELID,觸發器所在表的 oid;
  • TG_TABLE_NAME,觸發器所在表的名稱;
  • TG_TABLE_SCHEMA,觸發器所在表的模式;
  • TG_NARGS,創建觸發器時傳遞給觸發器函數的參數個數;
  • TG_ARGV[],創建觸發器時傳遞給觸發器函數的具體參數,下標從 0 開始。非法的下標(小於 0 或者大於等於 tg_nargs)將會返回空值。

然後,使用 CREATE TRIGGER 語句創建一個觸發器:

CREATE TRIGGER trigger_name 
{BEFORE | AFTER | INSTEAD OF} {event [OR ...]}
   ON table_name
   [FOR [EACH] {ROW | STATEMENT}]
   [WHEN ( condition ) ]
   EXECUTE FUNCTION trigger_function;

其中,event 可以是 INSERT、UPDATE、DELETE 或者 TRUNCATE,UPDATE 支持特定字段(UPDATE OF col1, clo2)的更新操作;觸發器可以在事件之前(BEFORE)或者之後(AFTER)觸發,INSTEAD OF 只能用於替代視圖上的 INSERT、UPDATE 或者 DELETE 操作;FOR EACH ROW 表示行級觸發器,FOR EACH STATEMENT 表示語句級觸發器;WHEN 用於指定一個額外的觸發條件,滿足條件纔會真正支持觸發器函數。

接下來我們通過觸發器來實現記錄員工的信息變更歷史,首先創建一個歷史記錄表 employees_history:

create table employees_history (
    id serial primary key,
	employee_id int null,
	first_name varchar(20) null,
	last_name varchar(25) null,
	email varchar(25) null,
	phone_number varchar(20) null,
	hire_date date null,
	job_id varchar(10) null,
	salary numeric(8,2) null,
	commission_pct numeric(2,2) null,
	manager_id int null,
	department_id int null,
	action_type varchar(10) not null,
	change_dt timestamp not null
);

然後定義一個觸發器函數 track_employees_change:

create or replace function track_employees_change()
  returns trigger as
$$
begin
  if tg_op = 'INSERT' then
    insert into employees_history(employee_id, first_name, last_name, email, phone_number, 
                                  hire_date, job_id, salary, commission_pct, manager_id, 
                                  department_id, action_type, change_dt)
    values(new.employee_id, new.first_name, new.last_name, new.email, new.phone_number, 
           new.hire_date, new.job_id, new.salary, new.commission_pct, new.manager_id, 
           new.department_id, 'INSERT', current_timestamp);
  elsif tg_op = 'UPDATE' then
    insert into employees_history(employee_id, first_name, last_name, email, phone_number, 
                                  hire_date, job_id, salary, commission_pct, manager_id, 
                                  department_id, action_type, change_dt)
    values(old.employee_id, old.first_name, old.last_name, old.email, old.phone_number, 
           old.hire_date, old.job_id, old.salary, old.commission_pct, old.manager_id, 
           old.department_id, 'UPDATE', current_timestamp);
  elsif tg_op = 'DELETE' then
    insert into employees_history(employee_id, first_name, last_name, email, phone_number, 
                                  hire_date, job_id, salary, commission_pct, manager_id, 
                                  department_id, action_type, change_dt)
    values(old.employee_id, old.first_name, old.last_name, old.email, old.phone_number, 
           old.hire_date, old.job_id, old.salary, old.commission_pct, old.manager_id, 
           old.department_id, 'DELETE', current_timestamp); 
  end if;

  return new;
end; $$
language plpgsql;

該函數根據不同的操作記錄了相應的歷史信息、操作類型和操作時間。

最後創建一個觸發器 trg_employees_change,將該函數與 employees 進行關聯:

create trigger trg_employees_change
  before insert or update or delete
  on employees
  for each row
  execute function track_employees_change();

至此,我們完成了觸發器的創建。接下來進行一些數據測試:

insert into employees(employee_id, first_name, last_name, email, phone_number, hire_date, job_id, salary, commission_pct, manager_id, department_id)
values(207, 'Tony', 'Dong', 'TonyDong', '01066665678', '2020-05-25', 'IT_PROG', 6000, null, 103, 60);

select * from employees_history;
id|employee_id|first_name|last_name|email   |phone_number|hire_date |job_id |salary |commission_pct|manager_id|department_id|action_type|change_dt          |
--|-----------|----------|---------|--------|------------|----------|-------|-------|--------------|----------|-------------|-----------|-------------------|
 1|        207|Tony      |Dong     |TonyDong|01066665678 |2020-05-25|IT_PROG|6000.00|              |       103|           60|INSERT     |2020-05-25 15:45:17|

在 employees 中插入一條記錄之後,employees_history 記錄了這一操作歷史;對於 UPDATE 和 DELETE 操作也是如此。

管理觸發器

PostgreSQL 提供了 ALTER TRIGGER 語句,用於修改觸發器:

ALTER TRIGGER name ON table_name RENAME TO new_name;

這種方式目前只支持修改觸發器的名稱,修改觸發器函數的方法和修改普通函數相同。

PostgreSQL 還支持觸發器的禁用和啓用:

ALTER TABLE table_name
{ENABLE | DISABLE} TRIGGER {trigger_name | ALL | USER};

默認創建的觸發器處於啓用狀態;我們也可以使用以上語句禁用或者啓用某個觸發器、某個表上的所有觸發器(ALL)或用戶觸發器(不包括內部生成的約束觸發器,例如用於外鍵約束或延遲唯一性約束以及排除約束的觸發器)。

📝視圖 information_schema.triggers 中存儲了關於觸發器的信息。

刪除觸發器

被禁用的觸發器仍然存在,只是不會被觸發;如果想要刪除觸發器,可以使用 DROP TRIGGER 語句:

DROP TRIGGER [IF EXISTS] trigger_name 
ON table_name [RESTRICT | CASCADE];

IF EXISTS 可以避免觸發器不存在時的錯誤提示;CASCADE 表示級聯刪除依賴於該觸發器的對象,RESTRICT 表示如果存在依賴於該觸發器的對象返回錯誤,默認爲 RESTRICT。

我們將 employees 表上的觸發器 trg_employees_change 刪除:

drop trigger trg_employees_change on employees;

雖然刪除了觸發器,但是觸發器函數 track_employees_change 仍然存在。

事件觸發器

除了數據變更觸發器之外,PostgreSQL 還提供了另一種觸發器:事件觸發器 。事件觸發器主要用於捕獲全局的 DDL 事件,目前支持 ddl_command_start、ddl_command_end、table_rewrite 和 sql_drop,這些事件支持的完整語句可以參考官方列表

對於事件觸發器的函數而言,同樣預定義了兩個變量:

  • TG_EVENT,觸發事件;
  • TG_TAG,觸發語句。

對於事件觸發器,首先也需要創建一個函數,返回類型爲 event_trigger。例如:

create or replace function abort_any_command()
  returns event_trigger
  as $$
begin
  if (user != 'postgres') then
    raise exception 'command % is disabled', tg_tag;
  end if;
end; $$
language plpgsql;

以上函數判斷當前操作用戶是否爲超級用戶(postgres),如果不是則不允許執行任何 DDL 語句。

接下來使用 CREATE EVENT TRIGGER 語句創建事件觸發器:

create event trigger abort_ddl on ddl_command_start
  execute function abort_any_command();

此時,如果使用非 postgres 用戶執行 DDL 語句時將會返回錯誤:

hrdb=# select user;
 user 
------
 tony
(1 row)

hrdb=# create table t(id int);
ERROR:  command CREATE TABLE is disabled
CONTEXT:  PL/pgSQL function abort_any_command() line 4 at RAISE

ALTER EVENT TRIGGER 語句可以啓用/禁用事件觸發器或者修改觸發器的名稱等:

ALTER EVENT TRIGGER name DISABLE;
ALTER EVENT TRIGGER name ENABLE;
ALTER EVENT TRIGGER name RENAME TO new_name;

DROP EVENT TRIGGER 語句可以用於刪除事件觸發器:

DROP EVENT TRIGGER [ IF EXISTS ] name [ CASCADE | RESTRICT ];

我們將事件觸發器 abort_ddl 刪除:

DROP EVENT TRIGGER abort_ddl;
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章