Programmatisk transaktionsstyring om foråret

1. Oversigt

Forårets @Transaktionel annotation giver en dejlig deklarativ API til at markere transaktionsgrænser.

Bag kulisserne tager et aspekt sig af oprettelse og vedligeholdelse af transaktioner, som de er defineret i hver forekomst af @Transaktionel kommentar. Denne tilgang gør det let at afkoble vores kerneforretningslogik fra tværgående bekymringer som transaktionsstyring.

I denne vejledning ser vi, at dette ikke altid er den bedste tilgang. Vi undersøger, hvilke programmatiske alternativer Spring tilbyder, ligesom Transaktionsskabelonog vores grunde til at bruge dem.

2. Problemer i paradis

Lad os antage, at vi blander to forskellige typer I / O i en simpel tjeneste:

@ Transaktionel offentlig ugyldig initialPayment (PaymentRequest anmodning) {savePaymentRequest (anmodning); // DB-opkaldThePaymentProviderApi (anmodning); // API updatePaymentState (anmodning); // DB saveHistoryForAuditing (anmodning); // DB}

Her har vi et par databaseopkald sammen med et muligvis dyrt REST API-opkald. Ved første øjekast kan det give mening at gøre hele metoden transaktionel, da vi måske vil bruge en EntityManager at udføre hele operationen atomisk.

Men hvis den eksterne API tager længere tid end normalt at svare, uanset årsag, kan vi snart løbe tør for databaseforbindelser!

2.1. Virkelighedens hårde natur

Her er hvad der sker, når vi kalder initialPayment metode:

  1. Det transaktionsmæssige aspekt skaber et nyt EntityManager og starter en ny transaktion - så låner den en Forbindelse fra forbindelsespoolen
  2. Efter det første databaseopkald ringer det til den eksterne API, mens den lånte bevares Forbindelse
  3. Endelig bruger det det Forbindelse for at udføre de resterende databaseopkald

Hvis API-opkaldet reagerer meget langsomt i et stykke tid, vil denne metode svine den lånte Forbindelse mens du venter på svaret.

Forestil dig, at i løbet af denne periode får vi en række opkald til initialPayment metode. Så alt Forbindelser kan vente på svar fra API-opkaldet. Derfor kan vi løbe tør for databaseforbindelser - på grund af en langsom back-end-service!

At blande database I / O med andre typer I / O i en transaktionel sammenhæng er en dårlig lugt. Så den første løsning på disse slags problemer er at adskille disse typer I / O helt. Hvis vi af en eller anden grund ikke kan adskille dem, kan vi stadig bruge Spring API'er til at administrere transaktioner manuelt.

3. Brug Transaktionsskabelon

Transaktionsskabelon leverer et sæt tilbagekaldsbaserede API'er til håndtering af transaktioner manuelt. For at bruge det skal vi først initialisere det med en PlatformTransactionManager.

For eksempel kan vi oprette denne skabelon ved hjælp af afhængighedsinjektion:

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

Det PlatformTransactionManager hjælper skabelonen med at oprette, begå eller returnere transaktioner.

Når du bruger Spring Boot, en passende bønne af typen PlatformTransactionManager registreres automatisk, så vi skal bare indsprøjte det. Ellers skal vi manuelt registrere en PlatformTransactionManager bønne.

3.1. Eksempel på domænemodel

Fra nu af vil vi af hensyn til demonstrationen bruge en forenklet betalingsdomænemodel. I dette enkle domæne har vi en Betaling enhed til at indkapsle hver betalings detaljer:

@Entity offentlig klasse Betaling {@Id @GeneratedValue privat Lang id; privat Langt beløb; @Column (unik = sand) privat String referenceNumber; @Enumereret (EnumType.STRING) privat stat; // getters and setters public enum State {STARTED, FAILED, SUCCESSFUL}}

Vi kører også alle tests i en testklasse ved hjælp af Testcontainers-biblioteket til at køre en PostgreSQL-forekomst før hver testcase:

@DataJpaTest @Testcontainers @ActiveProfiles ("test") @AutoConfigureTestDatabase (erstatte = INGEN) @ Transactional (propagation = NOT_SUPPORTED) // vi vil håndtere transaktioner manuelt offentlig klasse ManualTransactionIntegrationTest {@Autowired private PlatformTransactionManager transactionManager; @Autowired privat 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. Transaktioner med resultater

Det Transaktionsskabelon tilbyder en metode kaldet udføre, som kan køre en given blok kode inde i en transaktion og derefter returnere noget resultat:

@Test ugyldigt 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 et nyt Betaling forekomst i databasen og derefter returnere dens auto-genererede id.

Svarende til den deklarative tilgang, skabelonen kan garantere atomicitet for os. Det vil sige, hvis en af ​​operationerne i en transaktion ikke gennemføres, er detruller dem alle tilbage:

@Test ugyldigt 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 referencenummer second.setState (Payment.State.SUCCESSFUL); entityManager.persist ( første); // ok entityManager.persist (anden); // mislykkes returnering "Ref-1";}); } fangst (undtagelse ignoreret) {} assertThat (entityManager.createQuery ("vælg p fra betaling p"). getResultList ()). er tom (); }

Siden det andet referencenummer er en duplikat, afviser databasen den anden vedvarende operation, hvilket får hele transaktionen til at returnere. Derfor indeholder databasen ingen betalinger efter transaktionen. Det er også muligt manuelt at udløse en tilbageførsel ved at ringe til setRollbackOnly () Transaktionsstatus:

@Test ugyldigt givenAPayment_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 ("vælg p fra betaling p"). getResultList ()). erEmpty (); }

3.3. Transaktioner uden resultater

Hvis vi ikke agter at returnere noget fra transaktionen, kan vi bruge TransactionCallbackWithoutResult tilbagekaldelsesklasse:

@Test ugyldigt 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 ("vælg p fra betaling p"). getResultList ()). har størrelse (1); }

3.4. Brugerdefinerede transaktionskonfigurationer

Indtil nu brugte vi Transaktionsskabelon med sin standardkonfiguration. Selvom denne standard er mere end nok det meste af tiden, er det stadig muligt at ændre konfigurationsindstillinger.

For eksempel kan vi indstille transaktionsisolationsniveauet:

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

På samme måde kan vi ændre transaktionsudbredelsesadfærden:

transactionTemplate.setPropagationBehavior (TransactionDefinition.PROPAGATION_REQUIRES_NEW);

Eller vi kan indstille en timeout i sekunder for transaktionen:

transactionTemplate.setTimeout (1000);

Det er endda muligt at drage fordel af optimeringer til skrivebeskyttede transaktioner:

transactionTemplate.setReadOnly (true);

Under alle omstændigheder, når vi opretter en Transaktionsskabelon med en konfiguration bruger alle transaktioner denne konfiguration til at udføre. Så, hvis vi har brug for flere konfigurationer, skal vi oprette flere skabelonforekomster.

4. Brug PlatformTransactionManager

Ud over Transaktionsskabelon, vi kan bruge et endnu lavere niveau som API PlatformTransactionManager til at administrere transaktioner manuelt. Ganske interessant, begge dele @Transaktionel og Transaktionsskabelon Brug denne API til at styre deres transaktioner internt.

4.1. Konfiguration af transaktioner

Før vi bruger denne API, skal vi definere, hvordan vores transaktion vil se ud. For eksempel kan vi indstille en timeout på tre sekunder med det gentagelige læsetransaktionsisolationsniveau:

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

Transaktionsdefinitioner ligner Transaktionsskabelon konfigurationer. Imidlertid, vi kan bruge flere definitioner med kun en PlatformTransactionManager.

4.2. Vedligeholdelse af transaktioner

Efter konfiguration af vores transaktion kan vi programmatisk administrere transaktioner:

@Test ugyldigt givenAPayment_WhenUsingTxManager_ThenShouldCommit () {// transaktionsdefinition TransactionStatus status = transactionManager.getTransaction (definition); prøv {Betalingsbetaling = ny betaling (); betaling.setReferenceNumber ("Ref-1"); payment.setState (Payment.State.SUCCESSFUL); entityManager.persist (betaling); transactionManager.commit (status); } fange (Undtagelse ex) {transactionManager.rollback (status); } assertThat (entityManager.createQuery ("vælg p fra betaling p"). getResultList ()). hasSize (1); }

5. Konklusion

I denne vejledning så vi først, hvornår man skulle vælge programmatisk transaktionsstyring frem for den deklarative tilgang. Derefter lærte vi ved at introducere to forskellige API'er, hvordan man manuelt opretter, begår eller tilbagefører en given transaktion.

Som sædvanlig er prøvekoden tilgængelig på GitHub.