PostgreSQL

Триггеры в PostgreSQL — CREATE TRIGGER, PL/pgSQL, NEW и OLD

Триггер — функция, автоматически вызываемая при INSERT, UPDATE или DELETE. Используется для аудита изменений, автоматического заполнения полей и валидации бизнес-правил. Разбираем синтаксис, переменные NEW/OLD и типовые паттерны.

Как работают триггеры

-- Триггер состоит из двух частей:
-- 1. Функция-обработчик (RETURNS trigger)
-- 2. Привязка функции к таблице и событию

-- Пример: автоматически заполнять updated_at при UPDATE:

-- Шаг 1: создать функцию:
CREATE OR REPLACE FUNCTION set_updated_at()
RETURNS trigger AS $$
BEGIN
  NEW.updated_at = NOW();
  RETURN NEW;   -- RETURN NEW для BEFORE триггеров
END;
$$ LANGUAGE plpgsql;

-- Шаг 2: привязать к таблице:
CREATE TRIGGER users_updated_at
  BEFORE UPDATE ON users
  FOR EACH ROW
  EXECUTE FUNCTION set_updated_at();

-- Теперь при любом UPDATE поле updated_at обновляется автоматически:
UPDATE users SET email = 'new@example.com' WHERE id = 1;
-- updated_at обновится без явного указания

Переменные NEW и OLD

-- NEW — новые значения строки (доступно в INSERT и UPDATE)
-- OLD — старые значения строки (доступно в UPDATE и DELETE)

-- Таблица доступности:
-- Событие   | NEW  | OLD
-- INSERT    | да   | нет
-- UPDATE    | да   | да
-- DELETE    | нет  | да

CREATE OR REPLACE FUNCTION audit_users()
RETURNS trigger AS $$
BEGIN
  IF TG_OP = 'INSERT' THEN
    INSERT INTO audit_log (table_name, operation, new_data, changed_at)
    VALUES ('users', 'INSERT', to_jsonb(NEW), NOW());
    RETURN NEW;

  ELSIF TG_OP = 'UPDATE' THEN
    INSERT INTO audit_log (table_name, operation, old_data, new_data, changed_at)
    VALUES ('users', 'UPDATE', to_jsonb(OLD), to_jsonb(NEW), NOW());
    RETURN NEW;

  ELSIF TG_OP = 'DELETE' THEN
    INSERT INTO audit_log (table_name, operation, old_data, changed_at)
    VALUES ('users', 'DELETE', to_jsonb(OLD), NOW());
    RETURN OLD;  -- для DELETE RETURN OLD

  END IF;
END;
$$ LANGUAGE plpgsql;

BEFORE vs AFTER триггеры

Тип Когда выполняется Может изменить NEW Применение
BEFORE До изменения строки Да (RETURN NEW) Валидация, автозаполнение полей, нормализация
AFTER После изменения строки Нет Аудит, уведомления, обновление агрегатов
INSTEAD OF Вместо операции (только для VIEW) Да Обновляемые VIEW с логикой
-- BEFORE INSERT — валидация и нормализация:
CREATE OR REPLACE FUNCTION normalize_email()
RETURNS trigger AS $$
BEGIN
  -- Привести email к нижнему регистру:
  NEW.email = lower(NEW.email);

  -- Проверить формат (простая проверка):
  IF NEW.email NOT LIKE '%@%.%' THEN
    RAISE EXCEPTION 'Неверный формат email: %', NEW.email;
  END IF;

  RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER users_normalize_email
  BEFORE INSERT OR UPDATE ON users
  FOR EACH ROW EXECUTE FUNCTION normalize_email();

-- AFTER INSERT — уведомление через NOTIFY:
CREATE OR REPLACE FUNCTION notify_new_order()
RETURNS trigger AS $$
BEGIN
  PERFORM pg_notify('new_order', row_to_json(NEW)::text);
  RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER orders_notify
  AFTER INSERT ON orders
  FOR EACH ROW EXECUTE FUNCTION notify_new_order();

Управление триггерами

-- Посмотреть все триггеры таблицы:
SELECT trigger_name, event_manipulation, action_timing, action_statement
FROM information_schema.triggers
WHERE event_object_table = 'users';

-- В psql:
\d users   -- в разделе "Triggers" видны все триггеры таблицы

-- Отключить триггер (например для bulk-загрузки):
ALTER TABLE users DISABLE TRIGGER users_updated_at;

-- Включить обратно:
ALTER TABLE users ENABLE TRIGGER users_updated_at;

-- Отключить все триггеры таблицы:
ALTER TABLE users DISABLE TRIGGER ALL;

-- Удалить триггер:
DROP TRIGGER users_updated_at ON users;
DROP TRIGGER IF EXISTS users_updated_at ON users;

-- Удалить функцию-обработчик (только если нет триггеров использующих её):
DROP FUNCTION set_updated_at();
DROP FUNCTION set_updated_at() CASCADE;  -- удалит и зависимые триггеры

Триггер для таблицы аудита

-- Создать таблицу аудита:
CREATE TABLE audit_log (
  id         BIGSERIAL PRIMARY KEY,
  table_name TEXT NOT NULL,
  operation  TEXT NOT NULL,  -- INSERT, UPDATE, DELETE
  old_data   JSONB,
  new_data   JSONB,
  changed_by TEXT DEFAULT current_user,
  changed_at TIMESTAMPTZ DEFAULT NOW()
);

-- Универсальная функция аудита:
CREATE OR REPLACE FUNCTION audit_table_changes()
RETURNS trigger AS $$
BEGIN
  INSERT INTO audit_log (table_name, operation, old_data, new_data)
  VALUES (
    TG_TABLE_NAME,   -- имя таблицы (автоматически из триггера)
    TG_OP,           -- INSERT, UPDATE или DELETE
    CASE WHEN TG_OP != 'INSERT' THEN to_jsonb(OLD) END,
    CASE WHEN TG_OP != 'DELETE' THEN to_jsonb(NEW) END
  );

  IF TG_OP = 'DELETE' THEN
    RETURN OLD;
  ELSE
    RETURN NEW;
  END IF;
END;
$$ LANGUAGE plpgsql;

-- Подключить к таблице:
CREATE TRIGGER audit_users
  AFTER INSERT OR UPDATE OR DELETE ON users
  FOR EACH ROW EXECUTE FUNCTION audit_table_changes();

CREATE TRIGGER audit_orders
  AFTER INSERT OR UPDATE OR DELETE ON orders
  FOR EACH ROW EXECUTE FUNCTION audit_table_changes();

Часто задаваемые вопросы

Когда использовать триггеры, а когда логику в приложении?

Триггеры хороши для: аудита изменений (гарантируют запись даже при прямом SQL), автозаполнения системных полей (updated_at, version), поддержания денормализованных агрегатов, валидации сложных бизнес-правил на уровне БД. Логика в приложении предпочтительнее для: бизнес-логики которую нужно тестировать, операций требующих внешних вызовов (HTTP, email), сложных условий зависящих от состояния приложения. Минус триггеров: неявное поведение затрудняет отладку, могут замедлить bulk-операции.

Почему триггер не срабатывает при TRUNCATE?

TRUNCATE не является row-level операцией — он удаляет все данные без обработки каждой строки. Триггеры FOR EACH ROW не срабатывают при TRUNCATE. Можно создать statement-level триггер: FOR EACH STATEMENT — тогда триггер сработает один раз для всей операции, но без доступа к OLD строкам. Если нужно перехватить TRUNCATE — используйте CREATE TRIGGER ... AFTER TRUNCATE ON table FOR EACH STATEMENT. Если нужен аудит удаления данных — используйте DELETE вместо TRUNCATE.

Как отладить триггер в PostgreSQL?

Используйте RAISE NOTICE для вывода отладочной информации: RAISE NOTICE 'NEW.email = %', NEW.email; — выведет в консоль psql. Для более сложной отладки: RAISE EXCEPTION откатит транзакцию и покажет сообщение. Проверьте что триггер вообще вызывается: временно добавьте INSERT INTO debug_log VALUES (TG_OP, NOW());. Посмотреть все триггеры: SELECT * FROM information_schema.triggers WHERE event_object_table = 'ваша_таблица';. Проверить отключённые триггеры: SELECT tgname, tgenabled FROM pg_trigger WHERE tgrelid = 'users'::regclass;.

Подписаться
Уведомить о
guest
0 комментариев
Старые
Новые Популярные

Настройки cookie

0
Оставьте комментарий! Напишите, что думаете по поводу статьи.x