Короткий ответ
Триггеры БД гарантируют, что ни одно изменение не пройдёт мимо аудита независимо от того, через какой код/сервис оно сделано (включая ручные правки через psql), но усложняют миграции и могут быть «невидимы» для разработчиков, читающих только код приложения; аудит на уровне приложения более прозрачен и прост в сопровождении, но не защищает от изменений в обход этого кода.
Как это работает подробнее
Триггер AFTER INSERT/UPDATE/DELETE может автоматически писать старые/новые значения строки (через OLD/NEW) в таблицу аудита в той же транзакции, что и само изменение; приложение-уровневый подход явно формирует запись аудита в коде обработчика, часто вместе с бизнес-семантикой изменения.
Сравнение
- Компромисс. Наиболее надёжный по полноте вариант — комбинация триггеров (или CDC/Debezium в отдельное хранилище) для гарантии «ничего не прошло мимо» и приложения для обогащения бизнес-контекстом; выбор зависит от строгости требований комплаенса, объёма изменений (триггеры добавляют накладные расходы на каждую запись) и готовности команды поддерживать более сложную инфраструктуру.
Пример кода
example.sql
CREATE TABLE account_audit (
id BIGSERIAL PRIMARY KEY,
account_id INTEGER NOT NULL,
changed_by TEXT,
changed_at TIMESTAMPTZ NOT NULL DEFAULT now(),
old_balance NUMERIC(14,2),
new_balance NUMERIC(14,2)
);
CREATE OR REPLACE FUNCTION audit_account_balance() RETURNS TRIGGER AS $$
BEGIN
IF NEW.balance IS DISTINCT FROM OLD.balance THEN
INSERT INTO account_audit(account_id, changed_by, old_balance, new_balance)
VALUES (NEW.id, current_setting('app.current_user', true), OLD.balance, NEW.balance);
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_audit_account_balance
AFTER UPDATE ON accounts
FOR EACH ROW EXECUTE FUNCTION audit_account_balance();
-- Срабатывает независимо от того, через ORM или прямой SQL было изменениеЧто использовать на практике
- Практика. Триггерный подход предпочтителен, когда к таблице обращаются разные сервисы/скрипты не через единый слой приложения; приложение-уровневый — когда важен бизнес-контекст изменения. Event sourcing-lite даёт наиболее полную историю «почему», но требует серьёзного пересмотра архитектуры хранения.
Подводные камни
- Ошибка. Реализуют аудит только в коде приложения, не замечая, что администратор БД или скрипт миграции данных может менять данные напрямую, минуя аудит; переkладывают объём аудита в ту же БД без партиционирования и политики хранения, из-за чего таблица аудита сама становится проблемой.