Czasami transakcje przypominają bohaterów z filmów o superbohaterach. Ratują nasze bazy danych przed katastrofą podczas awarii, błędów czy problemów. Jeśli masz zadanie wymagające wykonania kilku operacji, których nie można rozdzielić, transakcje zapewnią ich wspólne wykonanie. Zobaczmy, jak to działa na przykładzie obsługi płatności.
Obsługa płatności
Wyobraź sobie klasyczną sytuację: masz dwa konta bankowe i chcesz przelać pieniądze z jednego na drugie. To nie jest po prostu operacja "jednym kliknięciem". Musisz upewnić się, że poprawnie pobrałeś środki z jednego konta i dodałeś je na drugie. Każdy błąd może prowadzić do katastrofy: albo oba konta pozostaną nietknięte, albo zaburzysz balans (np. pieniądze znikną lub pojawią się "z powietrza").
Scenariusz: Przelew pieniędzy między kontami
Oto nasz kod. Czytaj go uważnie, jakby to była wiadomość z odległej galaktyki:
-- Zaczynamy transakcję
BEGIN;
-- Krok 1. Odejmujemy pieniądze z konta nadawcy
UPDATE accounts
SET balance = balance - 100
WHERE account_id = 1;
-- Krok 2. Dodajemy pieniądze na konto odbiorcy
UPDATE accounts
SET balance = balance + 100
WHERE account_id = 2;
-- Wszystko poszło dobrze? To zapisujemy zmiany!
COMMIT;
Co tu jest ważne?
- Jeśli na
Kroku 1lubKroku 2coś pójdzie nie tak (np. błąd w zapytaniu, brak środków), transakcja może być cofnięta przezROLLBACK, a dane zostaną w stanie początkowym. COMMITgwarantuje, że zmiany zostaną zastosowane tylko jeśli WSZYSTKIE kroki się powiodą.
Dodajemy sprawdzenie salda
A co jeśli nadawca nie ma wystarczająco środków na przelew? Dodajmy sprawdzenie salda, żeby przypadkiem nie "wprowadzić go na minus".
-- Zaczynamy transakcję
BEGIN;
-- Pobieramy aktualne saldo nadawcy
DO $$
DECLARE
current_balance NUMERIC;
BEGIN
SELECT balance INTO current_balance FROM accounts WHERE account_id = 1;
-- Sprawdzamy, czy wystarczy pieniędzy
IF current_balance >= 100 THEN
-- Jeśli wystarczy, wykonujemy przelew
UPDATE accounts
SET balance = balance - 100
WHERE account_id = 1;
UPDATE accounts
SET balance = balance + 100
WHERE account_id = 2;
-- Zatwierdzamy zmiany
COMMIT;
ELSE
-- Jeśli nie wystarczy, cofamy
ROLLBACK;
RAISE NOTICE 'Za mało środków na przelew!';
END IF;
END $$;
Co tu już ciekawsze?
- Używamy bloku PL/pgSQL z warunkiem
IF. Jeśli saldo jest mniejsze niż potrzebna kwota, transakcja zostanie odrzucona i nic się nie zmieni. ROLLBACKcofa zmiany, jeśli zostały rozpoczęte (chociaż w tym przypadku nie ma jeszcze czego cofać, ale to dobry zwyczaj).
Ten wykład jest poświęcony prawdziwym scenariuszom użycia transakcji, więc postanowiłem podać tu przykład z życia wzięty. Zawiera procedurę składowaną i jest napisany z użyciem PL-SQL. Myślę, że masz już wystarczająco doświadczenia, żeby zrozumieć jak to działa. W przyszłości wrócimy do tematu PL-SQL i przerobimy nawet dużo bardziej złożone przykłady.
Masowa aktualizacja danych w transakcji
Tworzenie transakcji przydaje się nie tylko przy przelewach. Załóżmy, że mamy bazę sklepu internetowego, gdzie codziennie dziesiątki zamówień mogą zmieniać status, np. z "w dostawie" na "zakończone". Jak zaktualizować wiele rekordów naraz tak, żeby w razie awarii mieć możliwość cofnięcia zmian? Oczywiście, używając transakcji.
Przeanalizujmy jeszcze jeden scenariusz: aktualizacja statusów zamówień.
Oto przykład:
-- Zaczynamy transakcję
BEGIN;
-- Krok 1. Aktualizujemy zamówienia z przeszłą datą dostawy
UPDATE orders
SET status = 'completed'
WHERE delivery_date < CURRENT_DATE;
-- Krok 2. Informujemy o udanej aktualizacji
RAISE NOTICE 'Wszystkie statusy zamówień zostały zaktualizowane pomyślnie.';
-- Zatwierdzamy zmiany
COMMIT;
A jeśli coś pójdzie nie tak?
Zawsze istnieje ryzyko błędu. Na przykład, przypadkowo zapomnisz dodać warunek WHERE i teraz wszystkie zamówienia zmieniły status na completed. Żeby uniknąć takich sytuacji, ważne jest zakończenie transakcji lub jawne jej cofnięcie.
Przeanalizujmy scenariusz z cofnięciem:
-- Zaczynamy transakcję
BEGIN;
-- Krok 1. Próba aktualizacji zamówień bez warunku (o nie, błąd!)
UPDATE orders
SET status = 'completed';
-- Cofnięcie transakcji z powodu błędu
ROLLBACK;
-- Teraz zamówienia pozostały bez zmian
Dodajemy trochę "elastyczności" z SAVEPOINT
Nie zawsze trzeba cofać całą transakcję. Jeśli twój scenariusz składa się z kilku działań, możesz chcieć cofnąć tylko część. Tu z pomocą przychodzi SAVEPOINT.
Teraz nasz scenariusz wygląda tak: obsługa kilku kroków z możliwością cofnięcia jednego z nich.
Wyobraź sobie, że obsługujesz zamówienie składające się z kilku kroków: zdjęcie towarów z magazynu, aktualizacja statusu zamówienia, wysłanie powiadomienia do klienta. Jeśli powiadomienie nie zostanie wysłane poprawnie, chcesz cofnąć tylko ten krok, ale zachować zmiany w bazie.
-- Zacznij transakcję
BEGIN;
-- Krok 1. Odejmujemy towary z magazynu
UPDATE products
SET stock = stock - 1
WHERE product_id = 101;
-- Zapisujemy punkt przywracania
SAVEPOINT step1;
-- Krok 2. Aktualizujemy status zamówienia
UPDATE orders
SET status = 'shipped'
WHERE order_id = 202;
-- Próba wysłania powiadomienia do klienta
SAVEPOINT step2;
-- Ups, błąd podczas powiadamiania!
ROLLBACK TO SAVEPOINT step2;
-- Decydujemy, że bezpiecznie zakończyć transakcję
COMMIT;
Podsumowanie
Transakcje to nie tylko narzędzie techniczne, to gwarancja spójności twoich danych. Chronią przed "efektem domina", kiedy jeden błąd może zniszczyć cały system. Za każdym razem, gdy wykonujesz kilka powiązanych operacji, zapytaj siebie: "A co jeśli jedna z nich się wywali?" Jeśli odpowiedź brzmi "katastrofa", to czas użyć transakcji. Pamiętaj: lepiej poświęcić kilka minut na napisanie transakcji, niż kilka godzin na odzyskiwanie danych po awarii. Twoi użytkownicy (i twoje nerwy) podziękują ci za to!
GO TO FULL VERSION