PL/pgSQL можно использовать для определения триггерных
функций, реагирующих на изменения данных или события базы данных.
Триггерная функция создается с помощью команды CREATE FUNCTION
, при этом она объявляется как функция без аргументов с типом возвращаемого значения
триггер (для триггеров изменения данных) или
event_trigger (для триггеров событий базы данных).
Специальные локальные переменные с именами TG_ определяются автоматически для описания условия, послужившего причиной вызова.
something
A триггер изменения данных объявляется как функция без аргументов, возвращающая тип данных триггер.
Необходимо учитывать, что функция должна быть объявлена без аргументов, даже если
предполагается получение аргументов, указанных в команде CREATE TRIGGER
— такие аргументы передаются через переменную TG_ARGV, как описано
ниже.
Когда PL/pgSQL функция вызывается в качестве триггера, в блоке верхнего уровня автоматически создается несколько специальных переменных. Среди них:
NEW record #
новая строка базы данных для INSERT/UPDATE операций в триггерах
уровня строки. Данная переменная имеет значение null в триггерах уровня оператора
и для DELETE операций.
OLD record #
старая строка таблицы для UPDATE/DELETE операций в триггерах
уровня строки. Данная переменная имеет значение null в триггерах уровня оператора
и для INSERT операций.
TG_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) #идентификатор объекта таблицы, вызвавшей срабатывание триггера.
TG_RELNAME имя #
таблица, вызвавшая срабатывание триггера
срабатывание. Данная возможность является устаревшей и может быть удалена в одном из будущих
выпусков. Используйте TG_TABLE_NAME вместо неё.
TG_TABLE_NAME имя #таблица, вызвавшая срабатывание триггера.
TG_TABLE_SCHEMA имя #схема таблицы, вызвавшей срабатывание триггера.
TG_NARGS integer #
количество аргументов, переданных триггерной
функции в CREATE TRIGGER операторе.
TG_ARGV text[] #
аргументы из
the CREATE TRIGGER операторе.
Индексация начинается с 0. Некорректные
индексы (меньше 0 или больше либо равные tg_nargs)
приводят к значению NULL.
Триггерная функция должна возвращать либо NULL или значение типа record/row, имеющее структуру, точно соответствующую структуре таблицы, для которой сработал триггер.
Триггеры уровня строки, вызываемые BEFORE может возвращать значение null, чтобы сигнализировать менеджеру триггеров о необходимости пропустить оставшуюся часть операции для данной строки (т. е. последующие триггеры не вызываются, и
INSERT/UPDATE/DELETE не выполняется
для этой строки). Если возвращается значение, отличное от null,
выполнение операции продолжается с данным значением строки.
Возврат значения строки, отличного от исходного значения
параметра NEW изменяет строку, которая будет вставлена или обновлена. Таким образом, если функция триггера должна обеспечить нормальное выполнение вызывающего действия без изменения значения строки, 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. Однако любой из этих типов триггеров всё же может прервать выполнение всей операции, вызвав ошибку.
Пример 5.6.3 приведён пример триггерной функции на языке PL/pgSQL.
Пример 5.6.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
-- Проверка заполнения имени сотрудника (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();
Другой способ логирования изменений в таблице подразумевает создание новой таблицы, содержащей записи о каждой выполненной операции вставки, обновления или удаления. Такой подход можно рассматривать как аудит изменений в таблице. Пример 5.6.4 приведен пример триггерной функции для аудита в PL/pgSQL.
Пример 5.6.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();
В одном из вариантов предыдущего примера используется представление, объединяющее основную таблицу с таблицей аудита, для отображения времени последнего изменения каждой записи. Данный подход по-прежнему позволяет фиксировать полный журнал аудита изменений в таблице, но при этом предоставляет упрощенное представление журнала, в котором для каждой записи отображается только временная метка последнего изменения, полученная из данных аудита. Пример 5.6.5 приведен пример триггера аудита для представления в PL/pgSQL.
Пример 5.6.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
-- для отражения изменений, внесенных в таблицу 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', 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();
Одним из назначений триггеров является поддержка сводной таблицы на основе данных из другой таблицы. Полученную сводную таблицу можно использовать вместо исходной при выполнении определенных запросов — зачастую со значительно сокращенным временем выполнения. Этот метод обычно используется в хранилищах данных, где таблицы измеренных или наблюдаемых данных (так называемые таблицы фактов) могут иметь чрезвычайно большой объем. Пример 5.6.6 приведён пример триггерной функции на языке PL/pgSQL поддерживающая сводную таблицу для таблицы фактов в хранилище данных.
Пример 5.6.6. Оператор PL/pgSQL Триггерная функция для поддержки сводной таблицы
Описанная здесь схема частично основана на примере «Продуктовый магазин» из книги The Data Warehouse Toolkit Ральфа Кимболла.
--
-- Основные таблицы — измерение времени и факты продаж.
--
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.
--
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 'Обновление time_key: % -> % не допускается',
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;
-- Вставка или обновление итоговой строки с новыми значениями.
<>
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 команда присваивает имена одной или обеим переходным таблицам, после чего функция может обращаться к этим именам как к временным таблицам, доступным только для чтения.
Пример 5.6.7 приведен пример.
Пример 5.6.7. Аудит с использованием переходных таблиц
Данный пример приводит к тем же результатам, что и
Пример 5.6.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();
PL/pgSQL можно использовать для определения
событийные триггеры.
Digital Q.DataBase требует, чтобы функция, вызываемая в качестве триггера событий, была объявлена без аргументов и возвращала тип event_trigger.
Если PL/pgSQL функция вызывается как событийный триггер, в блоке верхнего уровня автоматически создается несколько специальных переменных, а именно:
Пример 5.6.8 содержит пример функции событийного триггера на языке PL/pgSQL.
Пример 5.6.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();