Порой транзакции напоминают персонажей супергеройских фильмов. Они спасают наши базы данных от бедствия при сбоях, ошибках или неполадках. Если вы работаете с задачей, требующей выполнения нескольких операций, которые нельзя разделять, транзакции обеспечат их единое выполнение. Давайте разберем, как это работает на примере обработки платежей.
Обработка платежей
Представьте классическую ситуацию: у вас есть два банковских счёта, и мы хотим перевести деньги с одного на другой. Это не просто операция "одной кнопкой". Нужно удостовериться, что мы корректно списали средства с одного счёта и добавили их на другой. Любая ошибка может привести к катастрофе: либо оба счёта останутся нетронутыми, либо нарушится баланс (например, деньги исчезнут или появятся "из воздуха").
Сценарий: Перевод денег между счетами
Вот наш код. Читаем его внимательно, словно это послание из далёкой галактики:
-- Начинаем транзакцию
BEGIN;
-- Шаг 1. Списываем деньги с аккаунта отправителя
UPDATE accounts
SET balance = balance - 100
WHERE account_id = 1;
-- Шаг 2. Добавляем деньги на аккаунт получателя
UPDATE accounts
SET balance = balance + 100
WHERE account_id = 2;
-- Всё прошло хорошо? Тогда сохраняем изменения!
COMMIT;
Что здесь важно?
- Если на
Шаге 1илиШаге 2что-то пошло не так (например, ошибка в запросе, нехватка средств), транзакция может быть откачена назад черезROLLBACK, и данные останутся в исходном состоянии. COMMITгарантирует, что изменения применятся только если ВСЕ шаги успешны.
Добавляем проверку баланса
А что если у отправителя недостаточно средств для перевода? Давайте добавим проверку баланса, чтобы случайно не "загнать его в минус".
-- Начинаем транзакцию
BEGIN;
-- Получаем текущий баланс отправителя
DO $$
DECLARE
current_balance NUMERIC;
BEGIN
SELECT balance INTO current_balance FROM accounts WHERE account_id = 1;
-- Проверяем, хватает ли денег
IF current_balance >= 100 THEN
-- Если денег хватает, выполняем перевод
UPDATE accounts
SET balance = balance - 100
WHERE account_id = 1;
UPDATE accounts
SET balance = balance + 100
WHERE account_id = 2;
-- Фиксируем изменения
COMMIT;
ELSE
-- Если денег не хватает, откатываем
ROLLBACK;
RAISE NOTICE 'Недостаточно средств для перевода!';
END IF;
END $$;
Что здесь уже интереснее?
- Мы используем блок PL/pgSQL с проверкой условия через
IF. Если баланс меньше необходимой суммы, транзакция будет отклонена и ничего не изменится. ROLLBACKотменяет изменения, если они были начаты (хотя в данном случае отменять пока нечего, но это хороший тон).
Эта лекция посвящена реальным сценарием испльзования транзакций, поэтому я решил привести тут пример из реальной жизни. Он содержит хранимую процедуру и написан с помощью PL-SQL. Я думаю что вам уже достаточно опыта, чтобы понять как тут все работает. В будущем мы вернемся к теме PL-SQL и разберем даже гораздо более сложные примеры.
Массовое обновление данных в транзакции
Создание транзакции полезно не только для задач с переводом средств. Допустим, у нас есть база данных интернет-магазина, где ежедневно десятки заказов могут менять свои статусы, например, с "в доставке" на "завершён". Как обновить множество записей разом так, чтобы в случае сбоя у нас оставалась возможность откатить изменения? Конечно же, использовать транзакцию.
Рассмотрим ещё один сценарий: обновление статусов заказов.
Вот пример:
-- Начинаем транзакцию
BEGIN;
-- Шаг 1. Обновляем заказы с датой доставки прошедшей
UPDATE orders
SET status = 'completed'
WHERE delivery_date < CURRENT_DATE;
-- Шаг 2. Уведомляем об успешном обновлении
RAISE NOTICE 'Все статусы заказов обновлены успешно.';
-- Применяем изменения
COMMIT;
А если что-то пошло не так?
Всегда есть вероятность ошибки. Например, вы случайно забыли указать условие WHERE, и теперь все заказы изменили свой статус на completed. Чтобы избежать таких ситуаций, важно завершить транзакцию или явно откатить её.
Рассмотрим сценарий с откатом:
-- Начинаем транзакцию
BEGIN;
-- Шаг 1. Попытка обновить заказы без условия (о нет, ошибка!)
UPDATE orders
SET status = 'completed';
-- Откат транзакции из-за ошибки
ROLLBACK;
-- Теперь заказы остались неизменными
Добавляем немного "гибкости" с SAVEPOINT
Не всегда нужно откатывать всю транзакцию. Если ваш сценарий состоит из нескольких действий, возможно, вам потребуется откатить только часть. Здесь на помощь приходит SAVEPOINT.
Теперь наш сценарий следующий: обработка нескольких шагов с возможностью отката одного из них.
Представьте, что вы обрабатываете заказ из нескольких шагов: списание товаров со склада, обновление статуса заказа, отправка уведомления клиенту. Если уведомление не отправилось успешно, вы хотите откатить только этот шаг, но сохранить изменения в базе.
-- Начать транзакцию
BEGIN;
-- Шаг 1. Списываем товары со склада
UPDATE products
SET stock = stock - 1
WHERE product_id = 101;
-- Сохраняем точку отката
SAVEPOINT step1;
-- Шаг 2. Обновляем статус заказа
UPDATE orders
SET status = 'shipped'
WHERE order_id = 202;
-- Попытка отправить уведомление клиенту
SAVEPOINT step2;
-- Ой, ошибка в процессе уведомления!
ROLLBACK TO SAVEPOINT step2;
-- Решаем, что завершить транзакцию безопасно
COMMIT;
Заключение
Транзакции — это не просто технический инструмент, это гарантия целостности ваших данных. Они защищают от "эффекта домино", когда одна ошибка может разрушить всю систему. Каждый раз, когда вы выполняете несколько связанных операций, спрашивайте себя: "А что, если одна из них упадёт?" Если ответ звучит как "катастрофа", значит, пора использовать транзакцию. Помните: лучше потратить несколько минут на написание транзакции, чем несколько часов на восстановление данных после сбоя. Ваши пользователи (и ваши нервы) скажут вам спасибо!
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ