Programmatisch transactiebeheer in het voorjaar

1. Overzicht

Lente @Transactional annotatie biedt een mooie declaratieve API om transactiegrenzen te markeren.

Achter de schermen zorgt een aspect voor het maken en onderhouden van transacties zoals ze worden gedefinieerd in elk exemplaar van het @Transactional annotatie. Deze aanpak maakt het gemakkelijk om onze kernlogica los te koppelen van transversale zorgen zoals transactiebeheer.

In deze tutorial zullen we zien dat dit niet altijd de beste aanpak is. We zullen onderzoeken welke programmatische alternatieven Spring biedt, zoals Transactietemplate, en onze redenen om ze te gebruiken.

2. Problemen in het paradijs

Stel dat we twee verschillende soorten I / O combineren in een eenvoudige service:

@Transactional public void initialPayment (PaymentRequest-verzoek) {savePaymentRequest (verzoek); // DB callThePaymentProviderApi (verzoek); // API updatePaymentState (verzoek); // DB saveHistoryForAuditing (verzoek); // DB}

Hier hebben we een paar databaseaanroepen naast een mogelijk dure REST API-aanroep. Op het eerste gezicht kan het zinvol zijn om de hele methode transactional te maken, aangezien we er misschien een willen gebruiken EntityManager om de hele operatie atomair uit te voeren.

Als die externe API echter om wat voor reden dan ook langer duurt dan normaal om te reageren, kunnen we binnenkort geen databaseverbindingen meer hebben!

2.1. De harde aard van de werkelijkheid

Dit is wat er gebeurt als we de voorschot methode:

  1. Het transactionele aspect creëert een nieuw EntityManager en start een nieuwe transactie - dus het leent er een Verbinding uit de verbindingspool
  2. Na de eerste databaseaanroep roept het de externe API op terwijl de geleende wordt behouden Verbinding
  3. Ten slotte gebruikt het dat Verbinding om de resterende databaseaanroepen uit te voeren

Als de API-oproep een tijdje erg traag reageert, zou deze methode het geleende in beslag nemen Verbinding in afwachting van het antwoord.

Stel je voor dat we tijdens deze periode een reeks telefoontjes krijgen naar het voorschot methode. Dan allemaal Verbindingen wacht mogelijk op een reactie van de API-aanroep. Daarom kunnen we geen databaseverbindingen meer hebben - vanwege een trage back-endservice!

Het mengen van de database-I / O met andere soorten I / O in een transactionele context is een slechte geur. De eerste oplossing voor dit soort problemen is dus om deze typen I / O helemaal te scheiden. Als we ze om welke reden dan ook niet kunnen scheiden, kunnen we nog steeds Spring API's gebruiken om transacties handmatig te beheren.

3. Met behulp van Transactietemplate

Transactietemplate biedt een set op callback-gebaseerde API's om transacties handmatig te beheren. Om het te gebruiken, moeten we het eerst initialiseren met een PlatformTransactionManager.

We kunnen deze sjabloon bijvoorbeeld instellen met behulp van afhankelijkheidsinjectie:

// test annotatieklasse ManualTransactionIntegrationTest {@Autowired private PlatformTransactionManager transactionManager; privé TransactionTemplate transactionTemplate; @BeforeEach void setUp () {transactionTemplate = nieuwe TransactionTemplate (transactionManager); } // weggelaten}

De PlatformTransactionManager helpt de sjabloon bij het maken, vastleggen of terugdraaien van transacties.

Bij gebruik van Spring Boot, een geschikte boon van het type PlatformTransactionManager wordt automatisch geregistreerd, dus we hoeven het alleen maar te injecteren. Anders moeten we handmatig een PlatformTransactionManager Boon.

3.1. Voorbeelddomeinmodel

Vanaf nu gaan we ter demonstratie een vereenvoudigd betalingsdomeinmodel gebruiken. In dit eenvoudige domein hebben we een Betaling entiteit om de details van elke betaling samen te vatten:

@Entity openbare klasse Betaling {@Id @GeneratedValue privé Lange id; privé Long bedrag; @Column (uniek = waar) private String referenceNumber; @Enumerated (EnumType.STRING) privéstaat; // getters en setters public enum State {STARTED, FAILED, SUCCESSFUL}}

We zullen ook alle tests binnen een testklasse uitvoeren, met behulp van de Testcontainers-bibliotheek om vóór elke testcase een PostgreSQL-instantie uit te voeren:

@DataJpaTest @Testcontainers @ActiveProfiles ("test") @AutoConfigureTestDatabase (replace = NONE) @Transactional (propagation = NOT_SUPPORTED) // we gaan transacties handmatig afhandelen public class ManualTransactionIntegrationTest {@Autowired private PlatformTransactionManager transactionManager; @Autowired private EntityManager entityManager; @Container privé statische PostgreSQLContainer pg = initPostgres (); privé TransactionTemplate transactionTemplate; @BeforeEach public void setUp () {transactionTemplate = nieuwe TransactionTemplate (transactionManager); } // test privé statische PostgreSQLContainer initPostgres () {PostgreSQLContainer pg = nieuwe PostgreSQLContainer ("postgres: 11.1") .withDatabaseName ("baeldung") .withUsername ("test") .withPassword ("test"); pg.setPortBindings (singletonList ("54320: 5432")); retourneer pg; }}

3.2. Transacties met resultaten

De Transactietemplate biedt een methode genaamd uitvoeren, die elk bepaald codeblok binnen een transactie kan uitvoeren en vervolgens een resultaat kan retourneren:

@Test ongeldig gegevenAPayment_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 payment.getId ();}); Betaling betaling = entityManager.find (Payment.class, id); assertThat (betaling) .isNotNull (); }

Hier houden we een nieuwe aan Betaling instantie in de database en retourneert vervolgens de automatisch gegenereerde id.

Net als bij de declaratieve benadering, het sjabloon kan atomiciteit garanderen voor ons. Dat wil zeggen, als een van de bewerkingen binnen een transactie niet wordt voltooid, wordt hetrolt ze allemaal terug:

@Test ongeldig gegevenTwoPayments_WhenRefIsDuplicate_ThenShouldRollback () {probeer {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"); // hetzelfde referentienummer second.setState (Payment.State.SUCCESSFUL); entityManager.persist ( first); // ok entityManager.persist (second); // mislukt return "Ref-1";}); } catch (uitzondering genegeerd) {} ‚Äč‚ÄčassertThat (entityManager.createQuery ("select p from Payment p"). getResultList ()). isEmpty (); }

Sinds de tweede referentienummer een duplicaat is, weigert de database de tweede persisterende bewerking, waardoor de hele transactie wordt teruggedraaid. Daarom bevat de database geen betalingen na de transactie. Het is ook mogelijk om handmatig een rollback te activeren door het setRollbackOnly () Aan Transactiestatus:

@Test ongeldig gegevenAPayment_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 payment.getId ();}); assertThat (entityManager.createQuery ("selecteer p van betaling p"). getResultList ()). isEmpty (); }

3.3. Transacties zonder resultaat

Als we niets van de transactie willen retourneren, kunnen we de TransactionCallbackWithoutResult callback klasse:

@Test ongeldig gegevenAPayment_WhenNotExpectingAnyResult_ThenShouldCommit () {transactionTemplate.execute (nieuwe TransactionCallbackWithoutResult () {@Override beschermd void doInTransactionWithoutResult (TransactionStatus-status) {PaymentTemplate.execute (nieuwe TransactionCallbackWithoutResult () {@Override beschermd void doInTransactionWithoutResult (TransactionStatus-status) {Payment-set = nieuwe Payment (); "payment.set .State.SUCCESSFUL); entityManager.persist (betaling);}}); assertThat (entityManager.createQuery ("selecteer p van betaling p"). getResultList ()). hasSize (1); }

3.4. Aangepaste transactieconfiguraties

Tot nu toe gebruikten we de Transactietemplate met zijn standaardconfiguratie. Hoewel deze standaardinstelling meestal meer dan voldoende is, is het nog steeds mogelijk om de configuratie-instellingen te wijzigen.

We kunnen bijvoorbeeld het transactie-isolatieniveau instellen:

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

Evenzo kunnen we het gedrag van de doorgifte van transacties wijzigen:

transactionTemplate.setPropagationBehavior (TransactionDefinition.PROPAGATION_REQUIRES_NEW);

Of we kunnen een time-out instellen, in seconden, voor de transactie:

transactionTemplate.setTimeout (1000);

Het is zelfs mogelijk om te profiteren van optimalisaties voor alleen-lezen transacties:

transactionTemplate.setReadOnly (true);

Hoe dan ook, zodra we een Transactietemplate met een configuratie zullen alle transacties die configuratie gebruiken om uit te voeren. Zo, als we meerdere configuraties nodig hebben, moeten we meerdere sjablooninstanties maken.

4. Met behulp van PlatformTransactionManager

Naast de Transactietemplate, we kunnen een API van een nog lager niveau gebruiken, zoals PlatformTransactionManager om transacties handmatig te beheren. Heel interessant, beide @Transactional en Transactietemplate gebruik deze API om hun transacties intern te beheren.

4.1. Transacties configureren

Voordat we deze API gebruiken, moeten we definiëren hoe onze transactie eruit zal zien. We kunnen bijvoorbeeld een time-out van drie seconden instellen met het herhaalbare leestransactie-isolatieniveau:

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

Transactiedefinities zijn vergelijkbaar met Transactietemplate configuraties. Echter, we kunnen meerdere definities gebruiken met slechts één PlatformTransactionManager.

4.2. Transacties bijhouden

Nadat we onze transactie hebben geconfigureerd, kunnen we transacties programmatisch beheren:

@ Test ongeldig gegevenAPayment_WhenUsingTxManager_ThenShouldCommit () {// transactiedefinitie TransactionStatus status = transactionManager.getTransaction (definitie); probeer {Payment payment = new Payment (); payment.setReferenceNumber ("Ref-1"); payment.setState (Payment.State.SUCCESSFUL); entiteitManager.persist (betaling); transactionManager.commit (status); } catch (uitzondering ex) {transactionManager.rollback (status); } assertThat (entityManager.createQuery ("selecteer p van betaling p"). getResultList ()). hasSize (1); }

5. Conclusie

In deze tutorial zagen we eerst wanneer men programmatisch transactiebeheer zou moeten kiezen boven de declaratieve benadering. Vervolgens hebben we door de introductie van twee verschillende API's geleerd hoe we een bepaalde transactie handmatig kunnen maken, vastleggen of terugdraaien.

Zoals gewoonlijk is de voorbeeldcode beschikbaar op GitHub.