Praktický návod
Jak pracovat s databázovými transakcemi
Do jedné krátké transakce zavři databázové změny, které musí uspět nebo selhat společně. Síťová volání nech mimo ni.
Nejdřív stručně
Jedna obchodní změna, jeden výsledek
Databázová transakce seskupí několik operací do jednoho celku. Commit je zveřejní společně, rollback je při chybě vrátí. Atomicita je první část vlastností ACID.
Transakce chrání jen práci provedenou stejným databázovým připojením. HTTP požadavek, e-mail ani zprávu odeslanou do cizí služby rollback nevrátí, proto externí I/O nepatří dovnitř otevřené transakce.
Připrav si
Co budeš potřebovat
Začni invariantem, který chceš chránit. Transakce není obecný obal kolem celého kontroleru.
- Doctrine ORM nebo DBAL nad databází s podporou transakcí.
- Jeden konkrétní use case, například převod kreditu nebo vytvoření objednávky a jejích položek.
- Popsaný invariant: co musí po commitu vždy platit a co se nesmí uložit jen napůl.
- Integrační databázi se stejným enginem a izolační úrovní jako produkce.
Kroky 1 až 3
Nastav krátkou a zřetelnou hranici
Načti potřebná data, ověř pravidla a ulož změny bez čekání na síť nebo uživatele.
1. Vymez atomickou databázovou operaci
- Sepiš všechny databázové zápisy, které nesmějí zůstat uložené jen částečně. Ty patří do jedné transakce.
- Validaci formátu a pomalé výpočty udělej před otevřením transakce, pokud nezávisí na právě zamčených datech.
- Uvnitř znovu ověř pravidla závislá na aktuálním stavu, například dostupný zůstatek nebo unikátnost rezervace.
- Neroztahuj hranici na celý HTTP request. Čím déle transakce běží, tím déle drží zámky a tím více roste riziko konfliktu.
2. Nech Doctrine řídit commit a rollback
- U ORM použij EntityManager::wrapInTransaction(). Doctrine při úspěchu provede flush a commit, při výjimce rollback.
- U čistého DBAL použij Connection::transactional(). Všechny dotazy musí běžet přes stejné Connection předané danému use case.
- Výjimku uvnitř callbacku nepolykej. Musí opustit transakční obal, aby se provedl rollback a volající poznal neúspěch.
- Po chybě ORM ověř stav EntityManageru. Doctrine ho může zavřít; pro další práci použij nový manager místo poškozené Unit of Work.
$entityManager->wrapInTransaction(static function (EntityManagerInterface $em): void { /* změny entit */ }); Oficiální Doctrine ORM dokumentace k transakcím 3. Odděl databázi od externích efektů
- Během otevřené transakce nevolej HTTP API, neposílej e-mail a nečekej na message broker. Cizí systém se rollbackem nevrátí.
- Jednoduchou následnou akci spusť až po úspěšném commitu a počítej s tím, že může samostatně selhat.
- Pokud událost nesmí zmizet, zapiš ji do outbox tabulky ve stejné transakci jako obchodní změnu. Samostatný worker ji odešle později.
- Příjemce zprávy navrhni idempotentně. Opakované doručení pak nevytvoří druhou platbu ani druhou objednávku.
Krok 4
Otestuj commit, rollback i souběh
Happy path ověří jen polovinu chování. Důležitější je, co v databázi zůstane po výjimce a při dvou současných požadavcích.
-
Ověř úspěšný commit
Spusť use case a v novém EntityManageru načti všechna změněná data. Musí společně splnit chráněný invariant.
php bin/phpunit --filter TransferMoney -
Vynuceně vyvolej chybu před commitem
Po prvním zápisu vyhoď testovací výjimku. V novém databázovém spojení ověř, že nezůstal ani první díl změny.
php bin/phpunit --filter RollsBack -
Pošli dvě konkurenční operace
Současně změň stejná data a ověř, že izolace, zámek nebo optimistická verze zabrání porušení invariantu.
Když to zlobí
Nejčastější chyby
Po výjimce zůstala část dat uložená
Zápisy pravděpodobně neběžely přes stejné spojení nebo část kódu commitla dřív. Přesuň všechny atomické zápisy pod jeden transakční obal.
Transakce drží zámky příliš dlouho
Přesuň HTTP, e-mail, souborové operace a výpočty mimo transakci. Uvnitř ponech jen nezbytné databázové čtení, kontrolu a zápis.
Aplikace po rollbacku hlásí zavřený EntityManager
Po selhání ORM transakce starý EntityManager znovu nepoužívej. Nech ho resetovat frameworkem a nový pokus spusť s čistou Unit of Work.
Vnořená metoda commitne dřív než vnější use case
Vlastníkem hranice má být aplikační use case. Vnitřní služby pouze mění data; nespoléhej na vnořenou transakci jako na nezávislý commit.
Hotovo
Databázová změna teď drží pohromadě.
Transakce má krátkou hranici, jasný invariant a spolehlivý rollback. U dalších use case nejdřív určuj, které databázové změny musí commitnout společně.