Бази Данни: Тригери (Triggers)

Автоматизация на процесите и запазване на цялостта на данните

1. Какво е Тригер?

Тригерът е специален вид съхранена процедура, която се стартира автоматично (задейства се), когато се случи определено събитие в базата данни.

Не можете да извикате тригер ръчно. Той чака "в засада" някой да промени данните.

INSERT / UPDATE / DELETE
ТРИГЕР
Изпълнение на логика

Основни компоненти:

2. Магията на NEW и OLD

Вътре в тригера имаме достъп до виртуални променливи, които съдържат данните от реда, който обработваме.

Променлива Описание Налична при
NEW.column_name Новата стойност, която се опитваме да запишем. INSERT, UPDATE
OLD.column_name Старата стойност, която е била в базата преди промяната. UPDATE, DELETE

🤔 Въпрос:

Искате да направите лог (архив) на старата заплата на служител, преди тя да бъде променена. Коя променлива ще използвате, за да вземете текущата (стара) заплата?

3. Пример: Валидация на данни (BEFORE)

Използваме BEFORE, когато искаме да проверим данните преди те да влязат в базата. Ако не ни харесват, можем да ги променим или да спрем заявката.

Задача: Не позволявай заплатата да бъде отрицателно число. Ако някой въведе -500, направи го 0.

CREATE TRIGGER check_salary_positive
BEFORE INSERT ON employees
FOR EACH ROW
BEGIN
    IF NEW.salary < 0 THEN
        SET NEW.salary = 0;
    END IF;
END;

🤔 Логика:

Защо използваме BEFORE в този случай, а не AFTER?

4. Пример: История на промените (AFTER)

Най-честата употреба на тригери е за Audit Logs. Когато някой изтрие или промени нещо, ние записваме "кой, какво и кога" в друга таблица.

Задача: Когато се изтрие служител, запиши името му в таблица deleted_employees_log.

CREATE TRIGGER log_employee_deletion
AFTER DELETE ON employees
FOR EACH ROW
BEGIN
    INSERT INTO deleted_employees_log (emp_name, date_deleted)
    VALUES (OLD.first_name, NOW());
END;

5. Задачи за упражнение

Задача 1: Защита на данните

Напишете тригер, който се задейства при обновяване (UPDATE) на таблицата products. Тригерът трябва да проверява дали новата цена (price) е по-висока от старата с повече от 100%. Ако е така, задайте новата цена да бъде старата цена + 10% (автоматична корекция).

CREATE TRIGGER prevent_price_hike
BEFORE UPDATE ON products
FOR EACH ROW
BEGIN
    IF NEW.price > OLD.price * 2 THEN
        SET NEW.price = OLD.price * 1.10;
    END IF;
END;

Задача 2: Автоматично попълване

Имате таблица tasks с колона completed_at (дата на завършване). Напишете тригер, който: при UPDATE на статус status = 'DONE', автоматично записва текущото време (NOW()) в колоната completed_at.

CREATE TRIGGER set_completion_date
BEFORE UPDATE ON tasks
FOR EACH ROW
BEGIN
    IF NEW.status = 'DONE' AND OLD.status != 'DONE' THEN
        SET NEW.completed_at = NOW();
    END IF;
END;

6. Workshop: Задачи за шампиони

Тези задачи изискват да комбинирате логика и синтаксис. За всяка задача има предоставена структура на таблиците.

Задача 3: Банкова сигурност (STOP Transaction)

Сценарий: Работите по банков софтуер. Имате таблица accounts с колони id, owner и balance.

Цел: Напишете тригер, който не позволява теглене на пари, ако балансът стане отрицателен. Ако след транзакцията балансът е под 0, тригерът трябва да върне баланса към старата му стойност (да отмени промяната).

Таблица: accounts (id, owner, balance)

Задача 4: История на цените (Audit Log)

Сценарий: В онлайн магазин искаме да следим как се променят цените на стоките във времето.

Цел: Създайте тригер, който при всяка промяна на цената в таблица products, записва старата цена, новата цена и датата на промяна в таблица price_history.

Таблици:
1. products (id, name, price)
2. price_history (id, product_id, old_price, new_price, changed_at)

Задача 5: Неизтриваемият запис (Protection)

Сценарий: В таблицата със служители има запис с позиция 'CEO' (Изпълнителен директор).

Цел: Напишете тригер, който предотвратява изтриването (DELETE) на служител, ако неговата длъжност (position) е 'CEO'. Ако някой се опита да го изтрие, тригерът трябва да хвърли грешка (или да използваме "трик" за MySQL, ако не ползваме SIGNAL - например да променим ID-то на невалидно, но тук ще ползваме SIGNAL SQLSTATE за пълнота).

Таблица: employees (id, name, position)