Kako u Spring Boot-u omogućiti podršku za transakcije?
O transakcijama
Transakcija je logički gledano skup operacija koje ili se izvršavaju, ili se ne izvršavaju ni jedna. Odnose se pre svega na baze podataka, na primer MySQL.
Dovoljno je zapamtiti samo to i razumevanje transakcija postaje vrlo lako. U Javi često moramo u okviru poslovne logike da obradimo više događaja; na primer, imamo metod za čuvanje članka koji pored samog članka mora da sačuva i njegove oznake. Oznake i članak ne žive u istoj tabeli, već se povezuju tako što se u tabeli članaka (posts) čuva primarni ključ oznake (tag_id) koji referencira tabelu oznaka (tags):
public void savePosts(PostsParam postsParam) {
// čuvanje članka
save(posts);
// obrada oznaka
insertOrUpdateTag(postsParam, posts);
}U tom trenutku potrebno je omogućiti transakciju, kako bi podaci u tabeli članaka i tabeli oznaka ostali usklađeni: ili se obe operacije izvrše, ili nijedna.
U suprotnom može doći do toga da članak bude uspešno sačuvan, ali oznake ne, ili da članak ne bude sačuvan, a oznake da jesu — svi takvi slučajevi nisu u skladu sa očekivanjima.
Da bi transakcija bila ispravna i pouzdana, prilikom upisa ili ažuriranja podataka u bazi moraju se ispoštovati 4 važne ACID karakteristike:
- Atomicity (atomičnost): sve operacije unutar jedne transakcije se ili izvrše sve, ili se ne izvrši nijedna, i ne ostaju u nekom međustanju. Ako tokom izvršavanja transakcije dođe do greške, vrši se povrat (Rollback) na stanje pre početka transakcije, kao da se ona nikada nije ni izvršila.
- Consistency (konzistentnost): pre početka i nakon završetka transakcije integritet baze podataka nije narušen.
- Isolation (izolovanost): baza podataka dozvoljava više istovremenih transakcija da istovremeno čitaju i menjaju podatke; izolovanost sprečava nekonzistentnost podataka koja bi nastala preklapanjem istovremenih transakcija.
- Durability (trajnost): nakon završetka transakcije, izmene podataka su trajne, ne gube se čak ni u slučaju kvara sistema.
Pored toga, izolovanost transakcija se deli na 4 različita nivoa:
- Read uncommitted (nepotvrđeno čitanje), najniži nivo izolacije, dozvoljava "prljava čitanja" (dirty reads) — transakcija može videti izmene koje su druge transakcije "još uvek nisu potvrdile". Ako druga transakcija nakon toga vrati izmene, podaci koje je trenutna transakcija pročitala biće prljavi.
- read committed (potvrđeno čitanje), jedna transakcija može naići na problem neponovljivog čitanja (Non Repeatable Read). Neponovljivo čitanje znači da se u okviru jedne transakcije više puta čita isti podatak; ako pre završetka te transakcije druga transakcija upravo izmeni taj podatak, dva čitanja u prvoj transakciji mogu dati različite rezultate.
- repeatable read (ponovljivo čitanje), jedna transakcija može naići na problem fantomskog čitanja (Phantom Read). Fantomsko čitanje znači da se u jednoj transakciji prilikom prvog traženja nekog zapisa utvrdi da on ne postoji, ali kada se pokuša sa ažuriranjem tog nepostojećeg zapisa to uspe, a prilikom ponovnog čitanja istog zapasa on se iznenada pojavi.
- Serializable (serijalizacija), najstroži nivo izolacije; sve transakcije se izvršavaju redom, jedna za drugom, pa prljava čitanja, neponovljiva čitanja ni fantomska čitanja ne mogu da se pojave. Iako transakcije pod Serializable nivoom izolacije imaju najveću bezbednost, s obzirom na to da se izvršavaju serijki, efikasnost znatno opada i performanse aplikacije drastično padaju. Osim u posebno važnim scenarijima, Serializable nivo izolacije se u pravilu ne koristi.
Posebno je važno naglasiti: da li će transakcija uopšte raditi zavisi od toga da li pogon baze podataka podržava transakcije — MySQL-ov InnoDB pogon podržava transakcije, dok MyISAM ne podržava.
O podršci za transakcije u Spring-u
Spring podržava dva načina rada sa transakcijama: programski (programmatic) i deklarativni (declarative), pri čemu je ovaj drugi najčešći. U većini slučajeva dovoljan je samo jedan @Transactional i stvar je rešena (uplivanje koda u poslovnu logiku je svedeno na minimum), ovako:
@Transactional
public void savePosts(PostsParam postsParam) {
// čuvanje članka
save(posts);
// obrada oznaka
insertOrUpdateTag(postsParam, posts);
}1) Programske transakcije
Programske transakcije podrazumevaju da se kod za upravljanje transakcijama ugradi direktno u poslovni kod, čime se kontrolisu potvrda (commit) i povrat (rollback) transakcije.
Na primer, korišćenje TransactionTemplate-a za upravljanje transakcijom:
@Autowired
private TransactionTemplate transactionTemplate;
public void testTransaction() {
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) {
try {
// .... poslovni kod
} catch (Exception e){
//povrat
transactionStatus.setRollbackOnly();
}
}
});
}Ili, korišćenje TransactionManager-a za upravljanje transakcijom:
@Autowired
private PlatformTransactionManager transactionManager;
public void testTransaction() {
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
// .... poslovni kod
transactionManager.commit(status);
} catch (Exception e) {
transactionManager.rollback(status);
}
}Što se tiče programskog upravljanja transakcijama, Spring preporučuje korišćenje TransactionTemplate-a.
Kod programskih transakcija, u svakoj poslovnoj operaciji mora se nalaziti dodatni kod za upravljanje transakcijama, pa kod izgleda veoma napučeno, ali je to vrlo korisno za razumevanje Spring-ovog modela upravljanja transakcijama.
2) Deklarativne transakcije
Deklarativne transakcije izdvajaju kod za upravljanje transakcijama iz poslovnih metoda i na deklarativan način ostvaruju upravljanje transakcijama; za programera je deklarativni pristup očigledno lakši za korišćenje i prijatniji od programskog.
Naravno, da bi se izdvojilo upravljanje transakcijama iz poslovnog koda, neophodna je jedna od najključnijih i najsržnijih tehnologija u Spring-u — AOP. Njena suština je presretanje pre i nakon metoda, zatim kreiranje ili pridruživanje transakciji pre početka ciljnog metoda, a nakon izvršenja ciljnog metoda vrši se potvrda ili povrat u zavisnosti od ishoda.
Iako su deklarativne transakcije bolje od programskih, imaju i manu: granularnost deklarativnog upravljanja transakcijama je na nivou metoda, dok programske transakcije mogu precizno da ciljaju na nivou bloka koda.
Model upravljanja transakcijama
Spring apstraktuje jezgro upravljanja transakcijama kroz menadžer transakcija (TransactionManager). Njegov izvorni kod sadrži samo jednostavnu definiciju interfejsa; u pitanju je marker interfejs:
public interface TransactionManager {
}Ovaj interfejs ima dva podinterfejsa: programske reaktive transakcije ReactiveTransactionManager i deklarativne transakcije PlatformTransactionManager. Fokusiraćemo se na PlatformTransactionManager, koji definiše 3 metoda:
interface PlatformTransactionManager extends TransactionManager{
// prema definiciji transakcije dobija status transakcije
TransactionStatus getTransaction(TransactionDefinition definition)
throws TransactionException;
// potvrda transakcije
void commit(TransactionStatus status) throws TransactionException;
// povrat transakcije
void rollback(TransactionStatus status) throws TransactionException;
}Preko PlatformTransactionManager interfejsa, Spring za svaku platformu — JDBC (DataSourceTransactionManager), Hibernate (HibernateTransactionManager), JPA (JpaTransactionManager) itd. — obezbeđuje odgovarajući menadžer transakcija, ali je konkretna implementacija stvar same platforme.
Parametar TransactionDefinition odgovara anotaciji @Transactional; na primer, svojstva koja se definišu u anotaciji @Transactional kao što su ponašanje propagacije transakcije, nivo izolacije, vreme isteka i to da li je transakcija samo za čitanje, sva se mogu naći u TransactionDefinition.
Povratni tip TransactionStatus uglavnom služi da čuva stanje i podatke tekuće transakcije, na primer resurse transakcije (connection), status povrata itd.
TransactionDefinition.java:
public interface TransactionDefinition {
// ponašanje propagacije transakcije
default int getPropagationBehavior() {
return PROPAGATION_REQUIRED;
}
// nivo izolacije transakcije
default int getIsolationLevel() {
return ISOLATION_DEFAULT;
}
// vreme isteka transakcije
default int getTimeout() {
return TIMEOUT_DEFAULT;
}
// da li je transakcija samo za čitanje
default boolean isReadOnly() {
return false;
}
}Transactional.java
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
Propagation propagation() default Propagation.REQUIRED;
Isolation isolation() default Isolation.DEFAULT;
int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;
boolean readOnly() default false;
}- propagation iz anotacije @Transactional odgovara getPropagationBehavior iz TransactionDefinition, sa podrazumevanom vrednošću
Propagation.REQUIRED(TransactionDefinition.PROPAGATION_REQUIRED). - isolation iz anotacije @Transactional odgovara getIsolationLevel iz TransactionDefinition, sa podrazumevanom vrednošću
DEFAULT(TransactionDefinition.ISOLATION_DEFAULT). - timeout iz anotacije @Transactional odgovara getTimeout iz TransactionDefinition, sa podrazumevanom vrednošću TransactionDefinition.TIMEOUT_DEFAULT.
- readOnly iz anotacije @Transactional odgovara isReadOnly iz TransactionDefinition, sa podrazumevanom vrednošću false.
Sada kada smo to razjasnili, detaljno ćemo objasniti ponašanje propagacije Spring transakcija, nivoe izolacije, vreme isteka, svojstvo samo-za-čitanje i pravila povrata transakcije.
Ponašanje propagacije transakcije
Kada se metod sa transakcijom pozove iz drugog metoda koji takođe ima transakciju, mora se definisati kako se transakcija propagira — na primer, metod može nastaviti da se izvršava u okviru tekuće transakcije, ili može otvoriti potpuno novu transakciju i izvršavati se u njoj.
Ponašanje propagacije deklarativne transakcije može se definisati preko svojstva propagation u anotaciji @Transactional, na primer:
@Transactional(propagation = Propagation.REQUIRED)
public void savePosts(PostsParam postsParam) {
}TransactionDefinition ukupno definiše 7 vrsta ponašanja propagacije:
PROPAGATION_REQUIRED
Ovo je podrazumevano ponašanje propagacije za @Transactional: ako trenutno postoji transakcija, pridružuje se njoj; ako ne postoji, kreira novu transakciju. Preciznije:
- Ako spoljni metod nije otvorio transakciju, unutrašnji metod modifikovan sa Propagation.REQUIRED otvoriće svoju vlastitu transakciju, i te transakcije su međusobno nezavisne i ne ometaju jedna drugu.
- Ako spoljni metod otvori transakciju i pri tome koristi Propagation.REQUIRED, svi unutrašnji metodi modifikovani sa Propagation.REQUIRED i spoljni metod pripadaju istoj transakciji; ako bilo koji metod izvrši povrat, cela transakcija mora da se vrati.
Class A {
@Transactional(propagation=Propagation.PROPAGATION_REQUIRED)
public void aMethod {
//radi nešto
B b = new B();
b.bMethod();
}
}
Class B {
@Transactional(propagation=Propagation.PROPAGATION_REQUIRED)
public void bMethod {
//radi nešto
}
}Ovo ponašanje propagacije je i najlakše za razumevanje: aMethod poziva bMethod i ako bilo koji od njih izvrši povrat, cela transakcija se vraća.
PROPAGATION_REQUIRES_NEW
Kreira novu transakciju; ako trenutno postoji transakcija, nju obustavlja. Drugim rečima, bez obzira na to da li spoljni metod otvara transakciju, unutrašnji metod modifikovan sa Propagation.REQUIRES_NEW otvoriće svoju vlastitu transakciju, i ta transakcija je nezavisna od spoljne, tako da se međusobno ne ometaju.
Class A {
@Transactional(propagation=Propagation.PROPAGATION_REQUIRED)
public void aMethod {
//radi nešto
B b = new B();
b.bMethod();
}
}
Class B {
@Transactional(propagation=Propagation.REQUIRES_NEW)
public void bMethod {
//radi nešto
}
}Ako aMethod() baci izuzetak i izvrši povrat, bMethod() ga ne prati, jer je bMethod() otvorio nezavisnu transakciju. Međutim, ako bMethod() baci neuhvaćeni izuzetak koji ispunjava pravila povrata transakcije, i aMethod() će takođe izvršiti povrat.
PROPAGATION_NESTED
Ako trenutno postoji transakcija, izvršava se u okviru nje; u suprotnom radi nešto slično kao PROPAGATION_REQUIRED.
PROPAGATION_MANDATORY
Ako trenutno postoji transakcija, pridružuje se njoj; ako ne postoji, baca izuzetak.
PROPAGATION_SUPPORTS
Ako trenutno postoji transakcija, pridružuje se njoj; ako ne postoji, nastavlja da radi bez transakcije.
PROPAGATION_NOT_SUPPORTED
Radi bez transakcije; ako trenutno postoji transakcija, obustavlja je.
PROPAGATION_NEVER
Radi bez transakcije; ako trenutno postoji transakcija, baca izuzetak.
Ponašanja 3, 4, 5, 6 i 7 se retko koriste, dovoljno je samo znati da postoje.
Nivoi izolacije transakcije
Pošto smo već upoznali nivoe izolacije transakcija u bazi podataka, mnogo je lakše razumeti nivoe izolacije u Spring-u.
TransactionDefinition definiše ukupno 5 nivoa izolacije:
- ISOLATION_DEFAULT, koristi podrazumevani nivo izolacije baze; MySQL podrazumevano koristi REPEATABLE_READ, odnosno ponovljivo čitanje.
- ISOLATION_READ_UNCOMMITTED, najniži nivo izolacije; mogu se pojaviti prljava čitanja, fantomska čitanja ili neponovljiva čitanja.
- ISOLATION_READ_COMMITTED, dozvoljava čitanje podataka koje su potvrdile istovremene transakcije; sprečava prljava čitanja, ali fantomska i neponovljiva čitanja još uvek mogu da se pojave.
- ISOLATION_REPEATABLE_READ, višestruka čitanja iste kolone daju konzistentne rezultate, osim ako podatke ne izmeni sama tekuća transakcija; sprečava prljava i neponovljiva čitanja, ali fantomsko čitanje i dalje može da se pojavi.
- ISOLATION_SERIALIZABLE, najviši nivo izolacije; iako sprečava prljava, fantomska i neponovljiva čitanja, ozbiljno utiče na performanse programa.
Uobičajajeno je dovoljno koristiti podrazumevani nivo izolacije ISOLATION_DEFAULT, odnosno prepustiti odluku bazi. Naredbom SELECT @@transaction_isolation; može se pročitati podrazumevani nivo izolacije u MySQL-u; rezultat je REPEATABLE-READ, odnosno ponovljivo čitanje.

Vreme isteka transakcije
Vreme isteka transakcije je najduže vreme koje se dozvoljava da se jedna transakcija izvršava; ako se ne završi u tom vremenu, automatski se vraća.
Ako se transakcija izvršava izuzetno dugo, s obzirom na to da uključuje zaključavanje u bazi podataka, dugotrajne transakcije će držati resurse baze.
Svojstvo samo-za-čitanje transakcije
Ako jedna transakcija samo čita iz baze, baza može iskoristiti to svojstvo samo-za-čitanje i primeniti optimizacije; korisno je kod višestrukih upita nad bazom.
Zašto uključivati podršku za transakcije za operaciju koja je samo čitanje?
Zato što MySQL (InnoDB) podrazumevano za svaku konekciju uključuje režim autocommit; u tom režimu, svaka SQL naredba poslata MySQL serveru obrađuje se u zasebnoj transakciji i automatski potvrđuje nakon izvršenja.
Ako na metod dodamo anotaciju @Transactional, sve SQL naredbe u tom metodu smestiće se u jednu transakciju. U suprotnom, svaka SQL naredba otvara zasebnu transakciju, pa izmene koje druge transakcije naprave u međuvremenu mogu se čitati u realnom vremenu.
U nekim situacijama, kada se u jednom koraku izvršava više upita i potrebno je garantirati konzistentnost podataka, neophodno je uključiti podršku za transakcije. U suprotnom, nakon prvog upita neki drugi korisnik izmeni podatke, pa sledeći upit može videti nekonzistentno stanje.
Pravila povrata transakcije
Podrazumevano, transakcija se vraća samo kada dođe do izuzetka za vreme izvršavanja (Runtime Exception) i Error-a; kod proveravanih izuzetaka (checked exception — koje treba eksplicitno uhvatiti ili proslediti nagore) ne vraća se.
Ako želite da vratite određeni tip izuzetka, možete podesiti ovako:
@Transactional(rollbackFor= MyException.class)O podršci za transakcije u Spring Boot-u
Ranije je bilo potrebno konfigurisati Spring putem XML-a da bi se transakcije preuzele; sa Spring Boot-om sve postaje mnogo jednostavnije — dovoljno je dodati anotaciju transakcije (@Transactional) na nivou poslovnog sloja i transakcija se brzo omogućava.
Drugim rečima, fokus možemo držati isključivo na anotaciji @Transactional.
Obim delovanja @Transactional
- Na klasi: ukazuje da sve public metode u klasi koriste transakcije.
- Na metodu: najčešća varijanta.
- Na interfejsu: nije preporučeno.
Uobičajajeni parametri konfiguracije @Transactional
Iako izvorni kod anotacije @Transactional definiše mnogo svojstava, u većini slučajeva koristim podrazumevanu konfiguraciju; naravno, ako je potrebno prilagoditi, to je već ranije opisano.
Napomene o korišćenju @Transactional
- Treba ga koristiti na public metodama. U metodu computeTransactionAttribute klase AbstractFallbackTransactionAttributeSource postoji provera: ako ciljni metod nije public, TransactionAttribute vraća null, odnosno transakcija nije podržana.
protected TransactionAttribute computeTransactionAttribute(Method method, @Nullable Class<?> targetClass) {
// Don't allow no-public methods as required.
if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
return null;
}
// The method may be on an interface, but we need attributes from the target class.
// If the target class is null, the method will be unchanged.
Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass);
// First try is the method in the target class.
TransactionAttribute txAttr = findTransactionAttribute(specificMethod);
if (txAttr != null) {
return txAttr;
}
// Second try is the transaction attribute on the target class.
txAttr = findTransactionAttribute(specificMethod.getDeclaringClass());
if (txAttr != null && ClassUtils.isUserLevelMethod(method)) {
return txAttr;
}
if (specificMethod != method) {
// Fallback is to look at the original method.
txAttr = findTransactionAttribute(method);
if (txAttr != null) {
return txAttr;
}
// Last fallback is the class of the original method.
txAttr = findTransactionAttribute(method.getDeclaringClass());
if (txAttr != null && ClassUtils.isUserLevelMethod(method)) {
return txAttr;
}
}
return null;
}- Izbegavajte pozivanje metoda anotiranih sa @Transactional iz iste klase, jer to vodi do toga da transakcija prestane da radi.
Više scenarija u kojima transakcija prestaje da radi možete pronaći na:
Provera da li transakcija radi
Pre testiranja, podićićemo podrazumevani nivo logovanja Spring Boot-a sa info na debug; izmena se radi u application.yml:
logging:
level:
org:
hibernate: debug
springframework:
web: debugZatim, pogledajmo podatke pročitane pre izmene:

Krećemo. U kontroler dodajte update interfejs; pripremamo izmenu podataka, nameravamo "Chenmo Wang Er-ovog podanika" da izmenimo u "Chenmo Wang Er-ovu nogu":
- codingmore: https://github.com/itwanger/coding-more
- Izvorni kod ovog projekta: https://github.com/itwanger/codingmore-learning
