Gestione delle transazioni programmatiche in primavera

1. Panoramica

L' annotazione @Transactional di Spring fornisce una bella API dichiarativa per contrassegnare i confini transazionali.

Dietro le quinte, un aspetto si occupa di creare e mantenere le transazioni così come sono definite in ogni occorrenza dell'annotazione @Transactional . Questo approccio rende facile separare la nostra logica aziendale di base da preoccupazioni trasversali come la gestione delle transazioni.

In questo tutorial vedremo che questo non è sempre l'approccio migliore. Esploreremo le alternative programmatiche fornite da Spring, come TransactionTemplate , e le nostre ragioni per utilizzarle.

2. Problemi in paradiso

Supponiamo di combinare due diversi tipi di I / O in un semplice servizio:

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

Qui, abbiamo alcune chiamate al database insieme a una chiamata API REST possibilmente costosa. A prima vista, potrebbe avere senso rendere l'intero metodo transazionale, poiché potremmo voler utilizzare un EntityManager per eseguire l'intera operazione in modo atomico.

Tuttavia, se l'API esterna impiega più tempo del solito a rispondere, per qualsiasi motivo, potremmo presto esaurire le connessioni al database!

2.1. La dura natura della realtà

Ecco cosa succede quando chiamiamo il metodo initialPayment :

  1. L'aspetto transazionale crea un nuovo EntityManager e avvia una nuova transazione, quindi prende in prestito una connessione dal pool di connessioni
  2. Dopo la prima chiamata al database, chiama l'API esterna mantenendo la connessione presa in prestito
  3. Infine, utilizza tale connessione per eseguire le restanti chiamate al database

Se la chiamata API risponde molto lentamente per un po ', questo metodo ostacolerebbe la connessione presa in prestito in attesa della risposta .

Immagina che durante questo periodo riceviamo una raffica di chiamate al metodo initialPayment . Quindi, tutte le connessioni potrebbero attendere una risposta dalla chiamata API. Ecco perché potremmo esaurire le connessioni al database, a causa di un servizio di back-end lento!

Mescolare l'I / O del database con altri tipi di I / O in un contesto transazionale è un cattivo odore. Quindi, la prima soluzione per questo tipo di problemi è quello di separare questi tipi di I / O del tutto . Se per qualsiasi motivo non siamo in grado di separarli, possiamo comunque utilizzare le API Spring per gestire manualmente le transazioni.

3. Utilizzo di TransactionTemplate

TransactionTemplate fornisce un set di API basate su callback per gestire manualmente le transazioni. Per usarlo, innanzitutto, dovremmo inizializzarlo con un PlatformTransactionManager.

Ad esempio, possiamo impostare questo modello utilizzando l'inserimento delle dipendenze:

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

Il PlatformTransactionManager aiuta il modello per creare, si impegnano, o le operazioni di rollback.

Quando si utilizza Spring Boot, un bean appropriato di tipo PlatformTransactionManager verrà registrato automaticamente, quindi è sufficiente iniettarlo. Altrimenti, dovremmo registrare manualmente un bean PlatformTransactionManager .

3.1. Modello di dominio di esempio

D'ora in poi, per motivi di dimostrazione, utilizzeremo un modello di dominio di pagamento semplificato. In questo semplice dominio, abbiamo un'entità di pagamento per incapsulare i dettagli di ogni pagamento:

@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 } }

Inoltre, eseguiremo tutti i test all'interno di una classe di test, utilizzando la libreria Testcontainers per eseguire un'istanza PostgreSQL prima di ogni test case:

@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. Transazioni con risultati

Le TransactionTemplate offre un metodo chiamato esecuzione, che può essere eseguito qualsiasi blocco di codice all'interno di una transazione e quindi tornare qualche risultato:

@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(); }

Qui, stiamo persistendo una nuova istanza di Payment nel database e quindi restituiamo il suo ID generato automaticamente.

Simile all'approccio dichiarativo, il modello può garantirci l'atomicità . Cioè, se una delle operazioni all'interno di una transazione non riesce a completarsi, essali ripristina tutti:

@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(); }

Poiché il secondo referenceNumber è un duplicato, il database rifiuta la seconda operazione di persistenza, causando il rollback dell'intera transazione. Pertanto, il database non contiene alcun pagamento dopo la transazione. È anche possibile attivare manualmente un rollback chiamando setRollbackOnly () su TransactionStatus:

@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. Transazioni senza risultati

Se non intendiamo restituire nulla dalla transazione, possiamo utilizzare la classe di callback TransactionCallbackWithoutResult :

@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. Configurazioni di transazione personalizzate

Fino ad ora, abbiamo utilizzato TransactionTemplate con la sua configurazione predefinita. Sebbene questa impostazione predefinita sia più che sufficiente per la maggior parte del tempo, è comunque possibile modificare le impostazioni di configurazione.

Ad esempio, possiamo impostare il livello di isolamento della transazione:

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

Allo stesso modo, possiamo modificare il comportamento di propagazione della transazione:

transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);

Oppure possiamo impostare un timeout, in secondi, per la transazione:

transactionTemplate.setTimeout(1000);

È anche possibile trarre vantaggio dalle ottimizzazioni per le transazioni di sola lettura:

transactionTemplate.setReadOnly(true);

Ad ogni modo, una volta creato un TransactionTemplate con una configurazione, tutte le transazioni utilizzeranno quella configurazione per essere eseguite. Quindi, se abbiamo bisogno di più configurazioni, dovremmo creare più istanze di modello .

4. Utilizzo di PlatformTransactionManager

Oltre a TransactionTemplate, possiamo utilizzare un'API di livello ancora inferiore come PlatformTransactionManager per gestire manualmente le transazioni. Abbastanza interessante, sia @Transactional che TransactionTemplate utilizzano questa API per gestire internamente le loro transazioni.

4.1. Configurazione delle transazioni

Prima di utilizzare questa API, dovremmo definire come apparirà la nostra transazione. Ad esempio, possiamo impostare un timeout di tre secondi con il livello di isolamento della transazione di lettura ripetibile:

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

Le definizioni delle transazioni sono simili alle configurazioni TransactionTemplate . Tuttavia , possiamo utilizzare più definizioni con un solo PlatformTransactionManager .

4.2. Gestione delle transazioni

Dopo aver configurato la nostra transazione, possiamo gestire in modo programmatico le transazioni:

@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. conclusione

In questo tutorial, in primo luogo, abbiamo visto quando si dovrebbe scegliere la gestione delle transazioni programmatiche rispetto all'approccio dichiarativo. Quindi, introducendo due API diverse, abbiamo imparato come creare, eseguire il commit o il rollback manualmente di una determinata transazione.

Come al solito, il codice di esempio è disponibile su GitHub.