41.10. Триггерные функции #
В PL/pgSQL можно создавать триггерные функции, которые будут вызываться при изменениях данных или событиях в базе данных. Триггерная функция создаётся командой CREATE FUNCTION
, при этом у функции не должно быть аргументов, а типом возвращаемого значения должен быть trigger
(для триггеров, срабатывающих при изменениях данных) или event_trigger
(для триггеров, срабатывающих при событиях в базе). Для триггеров автоматически определяются специальные локальные переменные с именами вида TG_
, описывающие условие, повлёкшее вызов триггера.имя
41.10.1. Триггеры при изменении данных #
Триггер при изменении данных объявляется как функция без аргументов и с типом результата 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
илиINSTEAD OF
в зависимости от определения триггера.TG_LEVEL
text
#ROW
илиSTATEMENT
в зависимости от определения триггера.TG_OP
text
#Операция, для которой сработал триггер:
INSERT
,UPDATE
,DELETE
илиTRUNCATE
.TG_RELID
oid
(ссылается наpg_class
.oid
) #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.
Триггерная функция должна вернуть либо NULL
, либо запись/строку, соответствующую структуре таблице, для которой сработал триггер.
Если BEFORE
триггер уровня строки возвращает NULL
, то все дальнейшие действия с этой строкой прекращаются (т. е. не срабат��вают последующие триггеры, команда INSERT
/UPDATE
/DELETE
для этой строки не выполняется). Если возвращается не NULL
, то дальнейшая обработка продолжается именно с этой строкой. Возвращение строки отличной от начальной NEW
, изменяет строку, которая будет вставлена или изменена. Поэтому, если в триггерной функции нужно выполнить некоторые действия и не менять саму строку, то нужно возвратить переменную NEW
(или её эквивалент). Для того чтобы изменить сохраняемую строку, можно поменять отдельные значения в переменной NEW
и затем её вернуть. Либо создать и вернуть полностью новую переменную. В случае строчного триггера BEFORE
для команды DELETE
само возвращаемое значение не имеет прямого эффекта, но оно должно быть отличным от NULL
, чтобы не прерывать обработку строки. Обратите внимание, что переменная NEW
всегда NULL
в триггерах на DELETE
, поэтому возвращать её не имеет смысла. Традиционной идиомой для триггеров DELETE
является возврат переменной OLD
.
Триггеры INSTEAD OF
(это всегда триггеры уровня строк и они могут применяться только с представлениями) могут возвращать NULL, чтобы показать, что они не выполняли никаких изменений, так что обработку этой строки можно не продолжать (то есть, не вызывать последующие триггеры и не считать строку в числе обработанных строк для окружающих команд INSERT
/UPDATE
/DELETE
). В противном случае должно быть возвращено значение, отличное от NULL, показывающее, что триггер выполнил запрошенную операцию. Для операций INSERT
и UPDATE
возвращаемым значением должно быть NEW
, которое триггерная функция может модифицировать для поддержки предложений INSERT RETURNING
и UPDATE RETURNING
(это также повлияет на значение строки, передаваемое последующим триггерам, или доступное под специальным псевдонимом EXCLUDED
в операторе INSERT
с предложением ON CONFLICT DO UPDATE
). Для операций DELETE
возвращаемым значением должно быть OLD
.
Возвращаемое значение для строчного триггера AFTER и триггеров уровня оператора (BEFORE или AFTER) всегда игнорируется. Это может быть и NULL. Однако в этих триггерах по-прежнему можно прервать вызвавшую их команду, для этого нужно явно вызвать ошибку.
Пример 41.3 показывает пример триггерной функции в PL/pgSQL.
Пример 41.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 -- Проверить, что указаны имя сотрудника и зарплата 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();
Другой вариант ведения журнала изменений для таблицы предполагает создание новой таблицы, которая будет содержать отдельную запись для к��ждой выполненной команды INSERT, UPDATE, DELETE. Этот подход можно рассматривать как протоколирование изменений таблицы для аудита. Пример 41.4 показывает реализацию соответствующей триггерной функции в PL/pgSQL.
Пример 41.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 -- -- Добавление строки в emp_audit, которая отражает операцию, выполняемую в emp, -- для определения типа операции применяется специальная переменная TG_OP. -- IF (TG_OP = 'DELETE') THEN INSERT INTO emp_audit SELECT 'D', now(), current_user, OLD.*; ELSIF (TG_OP = 'UPDATE') THEN INSERT INTO emp_audit SELECT 'U', now(), current_user, NEW.*; ELSIF (TG_OP = 'INSERT') THEN INSERT INTO emp_audit SELECT 'I', now(), current_user, NEW.*; END IF; RETURN NULL; -- возвращаемое значение для триггера AFTER игнорируется 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();
У предыдущего примера есть разновидность, которая использует представление, соединяющее основную таблицу и таблицу аудита, для отображения даты последнего изменения каждой строки. При этом подходе по-прежнему ведётся полный журнал аудита в отдельной таблице, но также имеется представление с упрощенным аудиторским следом. Это представление содержит временную метку, которая вычисляется для каждой строки из данных аудиторской таблицы. Пример 41.5 показывает пример триггера на представление для аудита в PL/pgSQL.
Пример 41.5. Триггерная функция на PL/pgSQL для аудита в представлении
В этом примере триггер, связанный с представлением, делает это представление изменяемым и гарантирует, что любая команда на добавление, изменение или удаление строки в представлении будет записана для аудита в таблицу 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 OR REPLACE FUNCTION update_emp_view() RETURNS TRIGGER AS $$ BEGIN -- -- Выполнить требуемую операцию в emp и добавить в emp_audit строку, -- отражающую эту операцию. -- 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', current_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', current_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', current_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();
Один из вариантов использования триггеров это поддержание в актуальном состоянии отдельной таблицы итогов для некоторой таблицы. В некоторых случаях отдельная таблица с итогами может использоваться в запросах вместо основной таблицы. При этом зачастую время выполнения запросов значительно сокращается. Эта техника широко используется в хранилищах данных, где таблицы фактов могут быть очень большими. Пример 41.6 демонстрирует триггерную функцию на PL/pgSQL, которая поддерживает таблицу итогов для таблицы фактов в хранилище данных.
Пример 41.6. Триггерная функция на PL/pgSQL для ведения таблицы итогов
Представленная здесь схема данных частично основана на примере Grocery Store из книги The Data Warehouse Toolkit Ральфа Кимбалла (Ralph Kimball).
-- -- Основные таблицы: таблица временных периодов и таблица фактов продаж -- 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); -- -- Функция и триггер для пересчёта столбцов итогов при выполнении -- команд INSERT, UPDATE, 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 -- Вычислить изменение количества/суммы. 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 -- Запретить изменение 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
также могут использовать переходные таблицы для просмотра всего набора строк, изменённых оператором, вызвавшим триггер. Команда CREATE TRIGGER
назначает имена одной или обеим переходным таблицам, а затем функция может по этим именам обращаться к ним как к временным таблицам только для чтения. Это иллюстрирует Пример 41.7.
Пример 41.7. Организация аудита с переходными таблицами
В данном примере достигается тот же результат, что и в Пример 41.4, но вместо триггера, срабатывающего для каждой строки, в нём используется триггер, срабатывающий единожды для оператора и получающий нужные ему данные в переходной таблице. Это может быть гораздо быстрее, чем вариант с построчным триггером, когда целевой оператор изменяет сразу множество строк. Заметьте, что мы должны объявить отдельные триггеры для каждого вида события, так как предложения 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() RETURNS TRIGGER AS $emp_audit$ BEGIN -- -- Добавление строк в emp_audit, которые отражают операции, выполняемые в emp, -- для определения типа операций применяется специальная переменная TG_OP. -- IF (TG_OP = 'DELETE') THEN INSERT INTO emp_audit SELECT 'D', now(), current_user, o.* FROM old_table o; ELSIF (TG_OP = 'UPDATE') THEN INSERT INTO emp_audit SELECT 'U', now(), current_user, n.* FROM new_table n; ELSIF (TG_OP = 'INSERT') THEN INSERT INTO emp_audit SELECT 'I', now(), current_user, n.* FROM new_table n; END IF; RETURN NULL; -- возвращаемое значение для триггера AFTER игнорируется 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();
41.10.2. Триггеры событий #
В PL/pgSQL можно создавать событийные триггеры. Postgres Pro требует, чтобы функция, которая вызывается как событийный триггер, объявлялась без аргументов и типом возвращаемого значения был event_trigger
.
Когда функция на PL/pgSQL вызывается как событийный триггер, в блоке верхнего уровня автоматически создаются несколько специальных переменных:
Пример 41.8 демонстрирует реализацию функции событийного триггера на PL/pgSQL.
Пример 41.8. Функция событийного триггера на PL/pgSQL
Триггер в этом примере просто выдаёт сообщение 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();