Programmatisk transaksjonsstyring om våren

1. Oversikt

Vårens @Transaksjonell kommentar gir en fin deklarativ API for å markere transaksjonsgrenser.

Bak kulissene tar et aspekt seg av å opprette og vedlikeholde transaksjoner slik de er definert i hver forekomst av @Transaksjonell kommentar. Denne tilnærmingen gjør det enkelt å koble vår kjernevirksomhetslogikk fra tverrgående bekymringer som transaksjonsstyring.

I denne opplæringen vil vi se at dette ikke alltid er den beste tilnærmingen. Vi vil undersøke hvilke programmatiske alternativer våren tilbyr, som TransactionTemplate, og våre grunner til å bruke dem.

2. Trøbbel i paradis

La oss anta at vi blander to forskjellige typer I / O i en enkel tjeneste:

@Transaksjonell offentlig ugyldig initialPayment (PaymentRequest forespørsel) {savePaymentRequest (forespørsel); // DB callThePaymentProviderApi (forespørsel); // API updatePaymentState (forespørsel); // DB saveHistoryForAuditing (forespørsel); // DB}

Her har vi noen få databasesamtaler ved siden av en mulig dyr REST API-samtale. Ved første øyekast kan det være fornuftig å gjøre hele metoden transaksjonell, siden vi kanskje vil bruke en EntityManager å utføre hele operasjonen atomisk.

Imidlertid, hvis den eksterne APIen tar lenger tid enn vanlig å svare, uansett årsak, kan vi snart gå tom for databaseforbindelser!

2.1. Virkelighetens harde natur

Her er hva som skjer når vi kaller initialPayment metode:

  1. Transaksjonsaspektet skaper et nytt EntityManager og starter en ny transaksjon - så, den låner en Forbindelse fra tilkoblingsbassenget
  2. Etter den første databasesamtalen ringer den til det eksterne API-et mens du holder det lånte Forbindelse
  3. Til slutt bruker den det Forbindelse for å utføre de gjenværende databasesamtalene

Hvis API-samtalen reagerer veldig sakte en stund, vil denne metoden svine den lånte Forbindelse mens du venter på svaret.

Tenk deg at i løpet av denne perioden får vi en rekke anrop til initialPayment metode. Så alt Tilkoblinger kan vente på svar fra API-samtalen. Det er derfor vi kan gå tom for databaseforbindelser - på grunn av en treg back-end-tjeneste!

Å blande databasens I / O med andre typer I / O i en transaksjonell sammenheng er en dårlig lukt. Så den første løsningen på denne typen problemer er å skille denne typen I / O helt. Hvis vi av en eller annen grunn ikke kan skille dem, kan vi fortsatt bruke Spring APIer til å administrere transaksjoner manuelt.

3. Bruke TransactionTemplate

TransactionTemplate gir et sett med tilbakeringingsbaserte API-er for å håndtere transaksjoner manuelt. For å kunne bruke den, bør vi først initialisere den med en PlatformTransactionManager.

For eksempel kan vi sette opp denne malen ved hjelp av avhengighetsinjeksjon:

// testkommentarer klasse ManualTransactionIntegrationTest {@Autowired private PlatformTransactionManager transactionManager; private TransactionTemplate transactionTemplate; @BeforeEach ugyldig setUp () {transactionTemplate = ny TransactionTemplate (transactionManager); } // utelatt}

De PlatformTransactionManager hjelper malen til å opprette, begå eller tilbakebetale transaksjoner.

Når du bruker Spring Boot, en passende bønne av typen PlatformTransactionManager blir automatisk registrert, så vi trenger bare å injisere den. Ellers bør vi registrere en PlatformTransactionManager bønne.

3.1. Eksempel på domenemodell

Fra nå av vil vi, for å demonstrere, bruke en forenklet betalingsdomenemodell. I dette enkle domenet har vi en innbetaling enhet for å kapsle inn detaljer om hver betaling:

@Entity offentlig klasse Betaling {@Id @GeneratedValue privat Lang id; privat Lang beløp; @Kolonne (unik = sann) privat strengreferansenummer; @Enumerated (EnumType.STRING) privat statlig stat; // getters and setters public enum State {STARTED, FAILED, SUCCESSFUL}}

Vi vil også kjøre alle testene i en testklasse ved å bruke Testcontainers-biblioteket til å kjøre en PostgreSQL-forekomst før hver testtilfelle:

@DataJpaTest @ Testcontainers @ ActiveProfiles ("test") @ AutoConfigureTestDatabase (erstatt = INGEN) @ Transactional (propagation = NOT_SUPPORTED) // vi skal håndtere transaksjoner manuelt offentlig klasse ManualTransactionIntegrationTest {@Autowired private PlatformTransactionManager transactionManager @Autowired private EntityManager entityManager; @Container privat statisk PostgreSQLContainer pg = initPostgres (); private TransactionTemplate transactionTemplate; @BeforeEach public void setUp () {transactionTemplate = new TransactionTemplate (transactionManager); } // tester privat statisk PostgreSQLContainer initPostgres () {PostgreSQLContainer pg = ny PostgreSQLContainer ("postgres: 11.1") .withDatabaseName ("baeldung") .withUsername ("test") .withPassword ("test"); pg.setPortBindings (singletonList ("54320: 5432")); retur pg; }}

3.2. Transaksjoner med resultater

De TransactionTemplate tilbyr en metode som heter henrette, som kan kjøre en gitt blokk med kode i en transaksjon og deretter returnere noe resultat:

@Test ugyldig givenAPayment_WhenNotDuplicate_ThenShouldCommit () {Long id = transactionTemplate.execute (status -> {Payment payment = new Payment (); payment.setAmount (1000L); payment.setReferenceNumber ("Ref-1"); payment.setState (Payment. State.SUCCESSFUL); entityManager.persist (betaling); return betaling.getId ();}); Betalingsbetaling = entityManager.find (Payment.class, id); assertThat (betaling) .isNotNull (); }

Her vedvarer vi en ny innbetaling forekomst i databasen og deretter returnere den automatisk genererte ID-en.

I likhet med den deklarative tilnærmingen, malen kan garantere atomisitet for oss. Det vil si at hvis en av operasjonene i en transaksjon ikke klarer å fullføre denruller dem alle tilbake:

@Test ugyldig givenTwoPayments_WhenRefIsDuplicate_ThenShouldRollback () {prøv {transactionTemplate.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"); // samme referansenummer second.setState (Payment.State.SUCCESSFUL); entityManager.persist ( første); // ok entityManager.persist (andre); // mislykkes retur "Ref-1";}); } fangst (unntak ignorert) {} assertThat (entityManager.createQuery ("velg p fra betaling p"). getResultList ()). er tom (); }

Siden den andre referanse nummer er et duplikat, avviser databasen den andre vedvarende operasjonen, og får hele transaksjonen til å tilbakestilles. Derfor inneholder databasen ingen betalinger etter transaksjonen. Det er også mulig å utløse en tilbakestilling manuelt ved å ringe setRollbackOnly () TransactionStatus:

@Test ugyldig gittAPayment_WhenMarkAsRollback_ThenShouldRollback () {transactionTemplate.execute (status -> {Payment payment = new Payment (); payment.setAmount (1000L); payment.setReferenceNumber ("Ref-1"); payment.setState (Payment.State.SUCCESSFUL ); entityManager.persist (betaling); status.setRollbackOnly (); return betaling.getId ();}); assertThat (entityManager.createQuery ("velg p fra betaling p"). getResultList ()). erEmpty (); }

3.3. Transaksjoner uten resultater

Hvis vi ikke har tenkt å returnere noe fra transaksjonen, kan vi bruke TransactionCallbackWithoutResult tilbakeringingsklasse:

@Test ugyldig givenAPayment_WhenNotExpectingAnyResult_ThenShouldCommit () {transactionTemplate.execute (new TransactionCallbackWithoutResult () {@Override protected void doInTransactionWithoutResult (TransactionStatus status) {Payment payment = new Payment (); payment.setReferenceNummer; " .State.SUCCESSFUL); entityManager.persist (betaling);}}); assertThat (entityManager.createQuery ("velg p fra betaling p"). getResultList ()). harSize (1); }

3.4. Egendefinerte transaksjonskonfigurasjoner

Inntil nå brukte vi TransactionTemplate med standardkonfigurasjonen. Selv om denne standarden er mer enn nok det meste av tiden, er det fortsatt mulig å endre konfigurasjonsinnstillinger.

For eksempel kan vi sette transaksjonsisolasjonsnivået:

transactionTemplate = ny TransactionTemplate (transactionManager); transactionTemplate.setIsolationLevel (TransactionDefinition.ISOLATION_REPEATABLE_READ);

På samme måte kan vi endre atferd for spredning av transaksjoner:

transactionTemplate.setPropagationBehavior (TransactionDefinition.PROPAGATION_REQUIRES_NEW);

Eller vi kan angi en tidsavbrudd i sekunder for transaksjonen:

transactionTemplate.setTimeout (1000);

Det er til og med mulig å dra nytte av optimaliseringer for skrivebeskyttede transaksjoner:

transactionTemplate.setReadOnly (true);

Uansett, når vi oppretter en TransactionTemplate med en konfigurasjon, vil alle transaksjoner bruke den konfigurasjonen til å utføre. Så, hvis vi trenger flere konfigurasjoner, bør vi opprette flere malforekomster.

4. Bruke PlatformTransactionManager

I tillegg til det Transaksjonskjema, Vi kan bruke et enda lavere nivå API PlatformTransactionManager for å administrere transaksjoner manuelt. Ganske interessant, begge deler @Transaksjonell og TransactionTemplate bruk dette API for å administrere sine transaksjoner internt.

4.1. Konfigurere transaksjoner

Før vi bruker denne APIen, bør vi definere hvordan transaksjonen vår vil se ut. For eksempel kan vi angi en tre sekunders tidsavbrudd med det repeterbare isolasjonsnivået for lesetransaksjon:

DefaultTransactionDefinition definisjon = ny DefaultTransactionDefinition (); definition.setIsolationLevel (TransactionDefinition.ISOLATION_REPEATABLE_READ); definition.setTimeout (3); 

Transaksjonsdefinisjoner ligner på TransactionTemplate konfigurasjoner. derimot, vi kan bruke flere definisjoner med bare én PlatformTransactionManager.

4.2. Opprettholde transaksjoner

Etter å ha konfigurert transaksjonen, kan vi programmatisk administrere transaksjoner:

@Test ugyldig givenAPayment_WhenUsingTxManager_ThenShouldCommit () {// transaksjonsdefinisjon TransactionStatus status = transactionManager.getTransaction (definisjon); prøv {Payment payment = new Payment (); payment.setReferenceNumber ("Ref-1"); payment.setState (Payment.State.SUCCESSFUL); entityManager.persist (betaling); transactionManager.commit (status); } fange (Unntak ex) {transactionManager.rollback (status); } assertThat (entityManager.createQuery ("velg p fra betaling p"). getResultList ()). harSize (1); }

5. Konklusjon

I denne veiledningen så vi først når man skulle velge programmatisk transaksjonsstyring fremfor den deklarative tilnærmingen. Deretter, ved å introdusere to forskjellige API-er, lærte vi hvordan vi manuelt oppretter, begår eller tilbakestiller en gitt transaksjon.

Eksempelkoden er som vanlig tilgjengelig på GitHub.


$config[zx-auto] not found$config[zx-overlay] not found