Spring - Transakce v Javě
V předchozím kvízu, Kvíz - REST API ve Spring Boot, jsme si ověřili nabyté zkušenosti z kurzu.
Transakce je posloupnost databázových příkazů, které se provedou v jednom běhu. Pokud se nějaký z nich neprovede (např. neprojde přes integritní omezení), neprovede se žádný z příkazů (všechno nebo nic).
Základní pojmy
- BEGIN - započne transakci
- COMMIT - všechny příkazy se vykonají postupně
- ROOLBACK - nastala chyba v nějakém příkazu. Data se vrátí do původního stavu (před začátkem transakce).
ACID
Pravidla, co musí každá transakce splňovat.
- Atomicity (atomicita) - transakce proběhne buď celá nebo neproběhne vůbec
- Consistency (konzistence) - při zapsání (vykonání příkazů) není porušeno žádné integritní omezení
- Isolation (izolovanost) - transakce je černá skříňka, operace se navzájem v rámci více transakcí neovlivní
- Durability (trvalost) – jednou uložené změny nezmizí a jsou uložené natrvalo
Spring
Transaction Manager
Řídí práci s transakcemi. V praxi se s ním setkáte jen výjimečně. Jde spíše o pochopení principu fungování.
public interface PlatformTransactionManager { TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException; void commit(TransactionStatus status) throws TransactionException; void rollback(TransactionStatus status) throws TransactionException; }
Nejspíše si říkáte, kde je metoda na zahájení transakce (begin). Metoda getTransaction je velmi specifická. Pokud transakce existuje vrátí ji, pokud ne vytvoří ji. Transakci zde reprezentuje třída TransactionStatus.
Nastavení ve Spring
Je potřeba zavést transakční manager do aplikačního kontextu:
<tx:annotation-driven transaction-manager="transactionManager"/> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/> </bean>
<tx:annotation-driven ... nám zajistí, že Spring bude číst anotace (@Transactional). Spring zařídí všechnu špinavou práci za nás (vytvoření proxy tříd...).
Nejspíše si povšimnete, že je potřeba transactionManager předat dataSource. DataSource slouží pro připojení aplikace k databázi. Sedlácky řečeno v něm nadefinujete adresu, jméno a heslo a on už se postará o připojení. Více se můžete dočíst v dokumentaci k dataSource
Bez anotaci
Pokud znáte princip fungování, nepřekvapí Vás nečekané situace. Při použití nástroje pro integritní testování jsme zjistili, že mechanismus pomocí anotací nefunguje podle očekávání. Vytvořili jsme tedy abstraktní třídu, která měla na starost řízení transakcí.
Integrační test
Testuje integraci (funkčnost propojení) více komponent dohromady.
Příklad: "Mám data v nějakém stavu. Provedu posloupnost příkazu napříč aplikací. Data se změnili podle očekávání.
public abstract class AbstractTransactionTest { @Autowired private DataSourceTransactionManager manager; private TransactionStatus transactionStatus; @Before public final void beforeTest() { //typ transakce int propagationBehavior = DefaultTransactionDefinition.PROPAGATION_REQUIRED; // vytvori inicialni stav transakce DefaultTransactionDefinition initStatus = new DefaultTransactionDefinition(propagationBehavior); // aktivuje transakci transactionStatus = transactionManager.getTransaction(initStatus); } @After public final void afterTest() { manager.rollback(transactionStatus); }
Princip je jednoduchý. Na začátku testu se zahájí transakce. Po dokončení testu se provede rollback.
Možná si říkáte, proč se provádí rollback... Jak jsme se dočetli v předchozích tutoriálech, testy by se neměli navzájem ovlivňovat. Pokud by se dostala data do databáze, mohlo by dojít ke komplikacím. Jiný test by mohl začínat s daty ve stavu, který neočekává.
Anotace
@Transactional(propagation ...) V praxi Vám tato anotace bohatě postačí na veškerou práci s transakcemi.
- Propagation.REQUIRED - DEFAULT. Pokud existuje transakce, je přejatá. Pokud neexistuje, je vytvořena nová.
- Propagation.MANDATORY - Pokud existuje transakce, je přejatá. Pokud neexistuje, vyhodí se výjimka.
- Propagation.NOT_SUPPORTED - Pozastaví běžící transakci a vykoná kód netransakčně.
- Propagation.SUPPORTED - Pokud existuje transakce, je přejatá. Pokud neexistuje, pustí se kód netrasakčně.
- Propagation.REQUIRES_NEW - Pokud existuje transakce, je pozastavena a vytvoří se nová. Pokud neexistuje, vytvoří se nová.
- Propagation.NEVER - Pokud existuje transakce, vyhodí výjimku. Jinak se vykoná netransakčně.
- Propagation.NESTED - Pokud existuje transakce, pustí novou (běží 2 paralelně). Pokud neexistuje, vytvoří se.
Jednoduché použití
Část kódu, která má běžet v transakci je označená danou anotací (@Transactional).
@Transactional public class CarService { public void myTransactionMethod() {} }
Jak to funguje?
Princip si ukážeme na imaginárním příkladu.
Anotace nám zajístí, že se nad danou třídou vytvoří proxy objekt (proxy pattern).
Daná metoda, co má běžet v transakci, je zaobalena do podobného bloku:
TransactionalManager txManager; public void myTransactionMethod(Object o) { try { TransactionStatus status = txManger.getTransaction(definition); myTransactionMethod(); txManager.commit(status); } catch (Exception e) { txManager.roolback(status); } }
pozn. Příklad vysvětluje jen princip fungování, v reálu je to složitější.
Kdykoli poté někdo zavolá danou třídu, zavolá se tento proxy objekt a vykoná se daný kód v transakci.
Pokud vše proběhne v naší metodě v pořádku (nevyhodí se výjimka), transakce manager provede commit transakce. Všechny příkazy se provedou nad databází.
Pokud v naší metodě dojde k chybě (např. kontrola duplicity záznamu vyhodí výjimku), manager provede roolback. Všechny příkazy po startu transakce se zahodí.
Architektura: Kde řídit transakci?
Neboli kde použít @Transactional. Pokud máte třívrstvou architekturu aplikace, transakce se obvykle řídí na vrstvě služby.

Třívrstvá architektura aplikace:
- Controller - slouží pro komunikaci s front-end. S tím co vidí uživatel.
- Service - vrstva, kde probíhají hlavní výpočty aplikace (zde se používá @Transactional)
- DAO - vrstva pro přístup do repository (databáze)