Programsko upravljanje transakcijama u proljeće

1. Pregled

Proljetni @Transational anotacija pruža lijep deklarativni API za označavanje transakcijskih granica.

Iza kulisa, aspekt se brine za stvaranje i održavanje transakcija onako kako su definirane u svakoj pojavi @Transational bilješka. Ovaj pristup olakšava razdvajanje naše osnovne poslovne logike od međusobnih problema poput upravljanja transakcijama.

U ovom uputstvu vidjet ćemo da ovo nije uvijek najbolji pristup. Istražit ćemo kakve programske alternative pruža Spring TransactionTemplatei naši razlozi za njihovo korištenje.

2. Nevolje u raju

Pretpostavimo da miješamo dvije različite vrste I / O u jednostavnoj usluzi:

@Transactional public void InitiPayment (zahtjev za zahtjev za plaćanjem) {savePaymentRequest (zahtjev); // DB callThePaymentProviderApi (zahtjev); // API updatePaymentState (zahtjev); // DB saveHistoryForAuditing (zahtjev); // DB}

Ovdje imamo nekoliko poziva baze podataka uz možda skup REST API poziv. Na prvi pogled možda bi imalo smisla učiniti cijelu metodu transakcijskom, jer bismo je možda željeli koristiti EntityManager da bi cijelu operaciju izveo atomski.

Međutim, ako na taj vanjski API treba više vremena nego što je uobičajeno da odgovori, iz bilo kojeg razloga, uskoro možemo ostati bez veza s bazom podataka!

2.1. Gruba priroda stvarnosti

Evo što se događa kada nazovemo početno plaćanje metoda:

  1. Transakcijski aspekt stvara novo EntityManager i započinje novu transakciju - dakle, posuđuje jednu Veza iz bazena veze
  2. Nakon prvog poziva baze podataka poziva vanjski API zadržavajući posuđeno Veza
  3. Konačno, to koristi Veza za obavljanje preostalih poziva baze podataka

Ako API poziv neko vrijeme reagira vrlo sporo, ova metoda će usmjeriti posuđeno Veza dok se čeka odgovor.

Zamislite da u tom razdoblju dobivamo niz poziva na početno plaćanje metoda. Onda, svi Veze može pričekati odgovor iz poziva API-ja. Zbog toga bismo mogli ostati bez veza s bazom podataka - zbog spore pozadinske usluge!

Miješanje U / I baze podataka s drugim vrstama U / I-a u transakcijskom kontekstu loš je miris. Dakle, prvo rješenje za ovakve probleme je potpuno odvajanje ovih vrsta I / O. Ako ih iz bilo kojeg razloga ne možemo razdvojiti, još uvijek možemo koristiti Spring API-je za ručno upravljanje transakcijama.

3. Korištenje TransactionTemplate

TransactionTemplate pruža skup API-ja temeljenih na povratnom pozivu za ručno upravljanje transakcijama. Da bismo ga mogli koristiti, prvo bismo ga trebali inicijalizirati s PlatformTransactionManager.

Na primjer, ovaj obrazac možemo postaviti pomoću ubrizgavanja ovisnosti:

// test klasa napomena ManualTransactionIntegrationTest {@Autowired private PlatformTransactionManageractionManager; private TransactionTemplateactionTemplate; @BeforeEach void setUp () {actionTemplate = novi TransactionTemplate (actionManager); } // izostavljeno}

The PlatformTransactionManager pomaže predlošku za stvaranje, predavanje ili vraćanje transakcija.

Kada koristite Spring Boot, odgovarajući grah tog tipa PlatformTransactionManager automatski će se registrirati, pa ga jednostavno trebamo ubrizgati. U suprotnom, trebali bismo ručno registrirati a PlatformTransactionManager grah.

3.1. Uzorak modela domene

Od sada ćemo, radi demonstracije, koristiti pojednostavljeni model domene plaćanja. U ovoj jednostavnoj domeni imamo Plaćanje entitet za uvrštavanje pojedinosti svake uplate:

@ Entity public class Payment {@Id @GeneratedValue private Long id; privatni Dugi iznos; @Kolona (jedinstveno = istinito) private String referenceNumber; @Enumerated (EnumType.STRING) privatna državna država; // getteri i postavljači državno nabrajanje Država {STARTED, FAILED, SUCCESSFUL}}

Također ćemo pokrenuti sve testove unutar klase testa, koristeći biblioteku Testcontainers za pokretanje instance PostgreSQL prije svakog test slučaja:

@DataJpaTest @Testcontainers @ActiveProfiles ("test") @AutoConfigureTestDatabase (replace = NONE) @Transactional (propagation = NOT_SUPPORTED) // ručno ćemo obrađivati ​​transakcije javne klase ManualTransactionIntegrationTest {@Autowired private PlatformTransactionManager; @Autowired private EntityManager entityManager; @Container privatni statički PostgreSQLContainer pg = initPostgres (); private TransactionTemplateactionTemplate; @BeforeEach public void setUp () {actionTemplate = new TransactionTemplate (actionManager); } // // testira privatni statički PostgreSQLContainer initPostgres () {PostgreSQLContainer pg = novi PostgreSQLContainer ("postgres: 11.1") .withDatabaseName ("baeldung") .withUsername ("test") .withPassword ("test"); pg.setPortBindings (singletonList ("54320: 5432")); povratak pg; }}

3.2. Transakcije s rezultatima

The TransactionTemplate nudi metodu tzv izvršiti, koji može pokrenuti bilo koji zadani blok koda unutar transakcije, a zatim vratiti neki rezultat:

@Test void givenAPayment_WhenNotDuplicate_ThenShouldCommit () {Long id =actionTemplate.execute (status -> {Payment Payment = new Payment (); Payment.setAmount (1000L); Payment.setReferenceNumber ("Ref-1"); Payment.setState (Payment. State.SUCCESSFUL); entityManager.persist (plaćanje); vratiti uplatu.getId ();}); Uplata plaćanja = entityManager.find (Payment.class, id); assertThat (plaćanje) .isNotNull (); }

Evo, ustrajemo na novom Plaćanje instance u bazu podataka, a zatim vraća njezin automatski generirani id.

Slično deklarativnom pristupu, predložak može jamčiti atomskost za nas. Odnosno, ako jedna od operacija unutar transakcije ne uspije dovršiti, onavraća ih sve:

@Test void givenTwoPayments_WhenRefIsDuplicate_ThenShouldRollback () {try {actionTemplate.execute (status -> {Payment first = new Payment (); first.setAmount (1000L); first.setReferenceNumber ("Ref-1"); first.setState (Payment.State) .SUCCESSFUL); Payment second = new Payment (); second.setAmount (2000L); second.setReferenceNumber ("Ref-1"); // isti referentni broj second.setState (Payment.State.SUCCESSFUL); entityManager.persist ( first); // ok entityManager.persist (second); // ne uspije vratiti "Ref-1";}); } catch (Iznimka se zanemaruje) {} assertThat (entityManager.createQuery ("select p from Payment p"). getResultList ()). isEmpty (); }

Od drugog referenceBroj je duplikat, baza podataka odbija drugu trajnu operaciju, što dovodi do vraćanja cijele transakcije. Stoga baza podataka ne sadrži plaćanja nakon transakcije. Također je moguće ručno pokrenuti vraćanje putem poziva setRollbackOnly () na TransactionStatus:

@Test void givenAPayment_WhenMarkAsRollback_ThenShouldRollback () {actionTemplate.execute (status -> {Payment Payment = new Payment (); Payment.setAmount (1000L); Payment.setReferenceNumber ("Ref-1"); Payment.setState (Payment.State.SUCCESSFUL) ); entityManager.persist (plaćanje); status.setRollbackOnly (); vratiti uplatu.getId ();}); assertThat (entityManager.createQuery ("odaberite p iz Payment p"). getResultList ()). isEmpty (); }

3.3. Transakcije bez rezultata

Ako ne namjeravamo ništa vratiti iz transakcije, možemo koristiti TransactionCallbackWithoutResult klasa povratnog poziva:

@Test void givenAPayment_WhenNotExpectingAnyResult_ThenShouldCommit () {actionTemplate.execute (new TransactionCallbackWithoutResult () {@Override protected void doInTransactionWithoutResult (TransactionStatus status) {Payment Payment = new Payment (Ref. 1). .State.SUCCESSFUL); entityManager.persist (plaćanje);}}); assertThat (entityManager.createQuery ("odaberite p iz Payment p"). getResultList ()). hasSize (1); }

3.4. Konfiguracije prilagođenih transakcija

Do sada smo koristili TransactionTemplate sa zadanom konfiguracijom. Iako je ova zadana vrijednost više nego dovoljna većinu vremena, ipak je moguće promijeniti postavke konfiguracije.

Na primjer, možemo postaviti razinu izolacije transakcije:

actionTemplate = novi TransactionTemplate (actionManager); actionTemplate.setIsolationLevel (TransactionDefinition.ISOLATION_REPEATABLE_READ);

Slično tome, možemo promijeniti ponašanje širenja transakcija:

actionTemplate.setPropagationBehavior (TransactionDefinition.PROPAGATION_REQUIRES_NEW);

Ili možemo postaviti vremensko ograničenje za transakciju u sekundama:

actionTemplate.setTimeout (1000);

Čak je moguće iskoristiti optimizacije za transakcije samo za čitanje:

actionTemplate.setReadOnly (true);

Svejedno, jednom kad stvorimo TransactionTemplate s konfiguracijom, sve će transakcije koristiti tu konfiguraciju za izvršavanje. Tako, ako trebamo više konfiguracija, trebali bismo stvoriti više primjeraka predloška.

4. Korištenje PlatformTransactionManager

Uz to TransactionTemplate, možemo koristiti API niže razine poput PlatformTransactionManager za ručno upravljanje transakcijama. Sasvim zanimljivo, oboje @Transational i TransactionTemplate koristite ovaj API za interno upravljanje njihovim transakcijama.

4.1. Konfiguriranje transakcija

Prije upotrebe ovog API-ja trebali bismo definirati kako će izgledati naša transakcija. Na primjer, možemo postaviti vremensko ograničenje od tri sekunde s ponovljivom razinom izolacije čitane transakcije:

Definicija DefaultTransactionDefinition = nova DefaultTransactionDefinition (); definition.setIsolationLevel (TransactionDefinition.ISOLATION_REPEATABLE_READ); definition.setTimeout (3); 

Definicije transakcija slične su TransactionTemplate konfiguracije. Međutim, možemo koristiti više definicija sa samo jednom PlatformTransactionManager.

4.2. Održavanje transakcija

Nakon konfiguriranja naše transakcije, možemo programski upravljati transakcijama:

@Test void givenAPayment_WhenUsingTxManager_ThenShouldCommit () {// definicija transakcije TransactionStatus status =actionManager.getTransaction (definicija); isprobajte {Payment Payment = new Payment (); Payment.setReferenceNumber ("Ref-1"); Payment.setState (Payment.State.SUCCESSFUL); entityManager.persist (plaćanje); actionManager.commit (status); } catch (Iznimka ex) {actionManager.rollback (status); } assertThat (entityManager.createQuery ("odaberite p iz Payment p"). getResultList ()). hasSize (1); }

5. Zaključak

Prvo smo u ovom vodiču vidjeli kada treba izabrati programsko upravljanje transakcijama umjesto deklarativnog pristupa. Zatim smo uvođenjem dva različita API-ja naučili kako ručno stvoriti, predati ili vratiti bilo koju zadanu transakciju.

Kao i obično, uzorak koda dostupan je na GitHubu.