Programmatisches Transaktionsmanagement im Frühjahr

1. Übersicht

Die @ Transactional- Annotation von Spring bietet eine nette deklarative API zum Markieren von Transaktionsgrenzen.

Hinter den Kulissen kümmert sich ein Aspekt um das Erstellen und Verwalten von Transaktionen, wie sie in jedem Auftreten der Annotation @Transactional definiert sind . Dieser Ansatz macht es einfach, unsere Kerngeschäftslogik von Querschnittsthemen wie dem Transaktionsmanagement zu entkoppeln.

In diesem Tutorial werden wir sehen, dass dies nicht immer der beste Ansatz ist. Wir werden untersuchen, welche programmatischen Alternativen Spring wie TransactionTemplate bietet und warum wir sie verwenden.

2. Ärger im Paradies

Nehmen wir an, wir mischen zwei verschiedene Arten von E / A in einem einfachen Dienst:

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

Hier haben wir einige Datenbankaufrufe neben einem möglicherweise teuren REST-API-Aufruf. Auf den ersten Blick kann es sinnvoll sein, die gesamte Methode transaktional durchzuführen, da wir möglicherweise einen EntityManager verwenden möchten , um die gesamte Operation atomar auszuführen.

Wenn die Antwort dieser externen API jedoch aus irgendeinem Grund länger als gewöhnlich dauert, gehen uns möglicherweise bald die Datenbankverbindungen aus!

2.1. Die raue Natur der Realität

Folgendes passiert, wenn wir die initialPayment- Methode aufrufen :

  1. Der Transaktionsaspekt erstellt einen neuen EntityManager und startet eine neue Transaktion. Daher wird eine Verbindung aus dem Verbindungspool ausgeliehen
  2. Nach dem ersten Datenbankaufruf wird die externe API aufgerufen, während die ausgeliehene Verbindung beibehalten wird
  3. Schließlich verwendet es , dass Anschluss die verbleibenden Datenbank Anrufe ausführen

Wenn der API-Aufruf für eine Weile sehr langsam reagiert, würde diese Methode die geliehene Verbindung belasten, während sie auf die Antwort wartet .

Stellen Sie sich vor, während dieser Zeit erhalten wir eine Reihe von Aufrufen der initialPayment- Methode. Dann warten alle Verbindungen möglicherweise auf eine Antwort vom API-Aufruf. Aus diesem Grund gehen uns möglicherweise die Datenbankverbindungen aus - aufgrund eines langsamen Back-End-Dienstes!

Das Mischen der Datenbank-E / A mit anderen E / A-Typen in einem Transaktionskontext ist ein schlechter Geruch. Die erste Lösung für diese Art von Problemen besteht darin, diese E / A-Typen vollständig zu trennen . Wenn wir sie aus irgendeinem Grund nicht trennen können, können wir Spring-APIs weiterhin verwenden, um Transaktionen manuell zu verwalten.

3. Verwenden von TransactionTemplate

TransactionTemplate bietet eine Reihe von Callback-basierten APIs zum manuellen Verwalten von Transaktionen. Um es zu verwenden, sollten wir es zuerst mit einem PlatformTransactionManager initialisieren .

Zum Beispiel können wir diese Vorlage mithilfe der Abhängigkeitsinjektion einrichten:

// test annotations class ManualTransactionIntegrationTest { @Autowired private PlatformTransactionManager transactionManager; private TransactionTemplate transactionTemplate; @BeforeEach void setUp() { transactionTemplate = new TransactionTemplate(transactionManager); } // omitted }

Der PlatformTransactionManager unterstützt die Vorlage beim Erstellen, Festschreiben oder Zurücksetzen von Transaktionen.

Bei Verwendung von Spring Boot wird automatisch eine entsprechende Bean vom Typ PlatformTransactionManager registriert, sodass wir sie nur einfügen müssen. Andernfalls sollten wir eine PlatformTransactionManager- Bean manuell registrieren .

3.1. Beispiel für ein Domänenmodell

Von nun an werden wir zur Demonstration ein vereinfachtes Zahlungsdomänenmodell verwenden. In dieser einfachen Domäne haben wir eine Zahlungseinheit , die die Details jeder Zahlung zusammenfasst:

@Entity public class Payment { @Id @GeneratedValue private Long id; private Long amount; @Column(unique = true) private String referenceNumber; @Enumerated(EnumType.STRING) private State state; // getters and setters public enum State { STARTED, FAILED, SUCCESSFUL } }

Außerdem führen wir alle Tests innerhalb einer Testklasse aus und verwenden die Testcontainers-Bibliothek, um vor jedem Testfall eine PostgreSQL-Instanz auszuführen:

@DataJpaTest @Testcontainers @ActiveProfiles("test") @AutoConfigureTestDatabase(replace = NONE) @Transactional(propagation = NOT_SUPPORTED) // we're going to handle transactions manually public class ManualTransactionIntegrationTest { @Autowired private PlatformTransactionManager transactionManager; @Autowired private EntityManager entityManager; @Container private static PostgreSQLContainer pg = initPostgres(); private TransactionTemplate transactionTemplate; @BeforeEach public void setUp() { transactionTemplate = new TransactionTemplate(transactionManager); } // tests private static PostgreSQLContainer initPostgres() { PostgreSQLContainer pg = new PostgreSQLContainer("postgres:11.1") .withDatabaseName("baeldung") .withUsername("test") .withPassword("test"); pg.setPortBindings(singletonList("54320:5432")); return pg; } }

3.2. Transaktionen mit Ergebnissen

Das TransactionTemplate bietet eine Methode namens execute, mit der ein beliebiger Codeblock innerhalb einer Transaktion ausgeführt und anschließend ein Ergebnis zurückgegeben werden kann:

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

Hier behalten wir eine neue Zahlungsinstanz in der Datenbank bei und geben dann ihre automatisch generierte ID zurück.

Ähnlich wie beim deklarativen Ansatz kann die Vorlage für uns Atomizität garantieren . Das heißt, wenn einer der Vorgänge innerhalb einer Transaktion nicht abgeschlossen werden kann, wird er ausgeführtrollt alle zurück:

@Test void givenTwoPayments_WhenRefIsDuplicate_ThenShouldRollback() { try { 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"); // same reference number second.setState(Payment.State.SUCCESSFUL); entityManager.persist(first); // ok entityManager.persist(second); // fails return "Ref-1"; }); } catch (Exception ignored) {} assertThat(entityManager.createQuery("select p from Payment p").getResultList()).isEmpty(); }

Da die zweite Referenznummer ein Duplikat ist, lehnt die Datenbank die zweite Persistenzoperation ab, wodurch die gesamte Transaktion zurückgesetzt wird. Daher enthält die Datenbank nach der Transaktion keine Zahlungen. Es ist auch möglich, einen Rollback manuell auszulösen, indem Sie setRollbackOnly () auf TransactionStatus aufrufen :

@Test void givenAPayment_WhenMarkAsRollback_ThenShouldRollback() { transactionTemplate.execute(status -> { Payment payment = new Payment(); payment.setAmount(1000L); payment.setReferenceNumber("Ref-1"); payment.setState(Payment.State.SUCCESSFUL); entityManager.persist(payment); status.setRollbackOnly(); return payment.getId(); }); assertThat(entityManager.createQuery("select p from Payment p").getResultList()).isEmpty(); }

3.3. Transaktionen ohne Ergebnisse

Wenn wir nicht beabsichtigen, etwas von der Transaktion zurückzugeben, können wir die TransactionCallbackWithoutResult- Rückrufklasse verwenden:

@Test void givenAPayment_WhenNotExpectingAnyResult_ThenShouldCommit() { transactionTemplate.execute(new TransactionCallbackWithoutResult() { @Override protected void doInTransactionWithoutResult(TransactionStatus status) { Payment payment = new Payment(); payment.setReferenceNumber("Ref-1"); payment.setState(Payment.State.SUCCESSFUL); entityManager.persist(payment); } }); assertThat(entityManager.createQuery("select p from Payment p").getResultList()).hasSize(1); }

3.4. Benutzerdefinierte Transaktionskonfigurationen

Up until now, we used the TransactionTemplate with its default configuration. Although this default is more than enough most of the time, it's still possible to change configuration settings.

For example, we can set the transaction isolation level:

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

Similarly, we can change the transaction propagation behavior:

transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);

Or we can set a timeout, in seconds, for the transaction:

transactionTemplate.setTimeout(1000);

It's even possible to benefit from optimizations for read-only transactions:

transactionTemplate.setReadOnly(true);

Anyway, once we create a TransactionTemplate with a configuration, all transactions will use that configuration to execute. So, if we need multiple configurations, we should create multiple template instances.

4. Using PlatformTransactionManager

In addition to the TransactionTemplate, we can use an even lower-level API like PlatformTransactionManager to manage transactions manually. Quite interestingly, both @Transactional and TransactionTemplate use this API to manage their transactions internally.

4.1. Configuring Transactions

Before using this API, we should define how our transaction is going to look. For example, we can set a three-second timeout with the repeatable read transaction isolation level:

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

Transaction definitions are similar to TransactionTemplate configurations. However, we can use multiple definitions with just one PlatformTransactionManager.

4.2. Maintaining Transactions

Nach der Konfiguration unserer Transaktion können wir Transaktionen programmgesteuert verwalten:

@Test void givenAPayment_WhenUsingTxManager_ThenShouldCommit() { // transaction definition TransactionStatus status = transactionManager.getTransaction(definition); try { Payment payment = new Payment(); payment.setReferenceNumber("Ref-1"); payment.setState(Payment.State.SUCCESSFUL); entityManager.persist(payment); transactionManager.commit(status); } catch (Exception ex) { transactionManager.rollback(status); } assertThat(entityManager.createQuery("select p from Payment p").getResultList()).hasSize(1); }

5. Schlussfolgerung

In diesem Tutorial haben wir zuerst gesehen, wann man das programmatische Transaktionsmanagement dem deklarativen Ansatz vorziehen sollte. Durch die Einführung von zwei verschiedenen APIs haben wir dann gelernt, wie eine bestimmte Transaktion manuell erstellt, festgeschrieben oder zurückgesetzt wird.

Wie üblich ist der Beispielcode auf GitHub verfügbar.