Gerenciamento programático de transações na primavera
1. Visão geral
A anotação@Transactional do Spring fornece uma boa API declarativa para marcar os limites transacionais.
Nos bastidores, um aspecto cuida da criação e manutenção das transações conforme são definidas em cada ocorrência da anotação@Transactional. Essa abordagem torna mais fácil separar nossa lógica de negócios principal decross-cutting concerns, como o gerenciamento de transações.
Neste tutorial, veremos que essa nem sempre é a melhor abordagem. Exploraremos as alternativas programáticas que o Spring oferece, comoTransactionTemplate, e nossos motivos para usá-las.
2. Problemas no paraíso
Suponhamos que estejamos misturando dois tipos diferentes de E / S em um serviço simples:
@Transactional
public void initialPayment(PaymentRequest request) {
savePaymentRequest(request); // DB
callThePaymentProviderApi(request); // API
updatePaymentState(request); // DB
saveHistoryForAuditing(request); // DB
}
Aqui, temos algumas chamadas de banco de dados juntamente com uma chamada de API REST possivelmente cara. À primeira vista, pode fazer sentido tornar todo o método transacional, uma vez que podemos querer usar umEntityManager para realizar a operação inteira atomicamente.
No entanto, se essa API externa demorar mais do que o normal para responder, por qualquer motivo, em breve poderemos ficar sem conexões com o banco de dados!
2.1. A dura natureza da realidade
Aqui está o que acontece quando chamamos o métodoinitialPayment :
-
O aspecto transacional cria um novoEntityManagere inicia uma nova transação - então, ele toma emprestado umConnection do pool de conexão
-
Após a primeira chamada de banco de dados, ele chama a API externa, mantendo oConnection emprestado
-
Finalmente, ele usa queConnection para realizar as chamadas de banco de dados restantes
If the API call responds very slowly for a while, this method would hog the borrowed Connection while waiting for the response.
Imagine que, durante esse período, tenhamos uma explosão de chamadas para o métodoinitialPayment . Então, todos osConnections podem esperar por uma resposta da chamada de API. That’s why we may run out of database connections — because of a slow back-end service!
Mixing the database I/O with other types of I/O in a transactional context is a bad smell. So, the first solution for these sorts of problems is to separate these types of I/O altogether. Se por algum motivo não podemos separá-los, ainda podemos usar APIs Spring para gerenciar as transações manualmente.
3. UsandoTransactionTemplate
TransactionTemplate oferece um conjunto de APIs baseadas em callback para gerenciar transações manualmente. Para usá-lo, primeiro devemos inicializá-lo com umPlatformTransactionManager.
Por exemplo, podemos configurar esse modelo usando injeção de dependência:
// test annotations
class ManualTransactionIntegrationTest {
@Autowired
private PlatformTransactionManager transactionManager;
private TransactionTemplate transactionTemplate;
@BeforeEach
void setUp() {
transactionTemplate = new TransactionTemplate(transactionManager);
}
// omitted
}
OPlatformTransactionManager armazena o modelo para criar, confirmar ou reverter transações.
Ao usar o Spring Boot, um bean apropriado do tipoPlatformTransactionManager erá registrado automaticamente, então precisamos simplesmente injetá-lo. Caso contrário, devemosmanually register aPlatformTransactionManager bean.
3.1. Modelo de Domínio de Amostra
A partir de agora, para fins de demonstração, usaremos um modelo de domínio de pagamento simplificado. Neste domínio simples, temosPayment ensibilidade para encapsular os detalhes de cada 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
}
}
Além disso, vamos executar todos os testes dentro de uma classe de teste, usando a bibliotecaTestcontainers para executar uma instância do PostgreSQL antes de cada caso de teste:
@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("example")
.withUsername("test")
.withPassword("test");
pg.setPortBindings(singletonList("54320:5432"));
return pg;
}
}
3.2. Transações com resultados
OTransactionTemplate oferece um método chamadoexecute, que pode executar qualquer bloco de código dentro de uma transação e retornar algum resultado:
@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();
}
Aqui, estamos persistindo uma novaPayment instance no banco de dados e, em seguida, retornando seu id gerado automaticamente.
Semelhante à abordagem declarativa,the template can guarantee atomicity para nós. Ou seja, se uma das operações dentro de uma transação não for concluída, retornará todas elas:
@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();
}
Since the second referenceNumber is a duplicate, the database rejects the second persist operation, causing the whole transaction to rollback. Portanto, o banco de dados não contém nenhum pagamento após a transação. Também é possível acionar manualmente uma reversão chamando osetRollbackOnly() onTransactionStatus:
@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. Transações sem resultados
Se não pretendemos retornar nada da transação, podemos usar a classe de retorno de chamadaTransactionCallbackWithoutResult:
@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. Configurações de transação personalizadas
Até agora, usamos oTransactionTemplate com sua configuração padrão. Embora esse padrão seja mais do que suficiente na maioria das vezes, ainda é possível alterar as configurações.
Por exemplo, podemos definir otransaction isolation level:
transactionTemplate = new TransactionTemplate(transactionManager);
transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_REPEATABLE_READ);
Da mesma forma, podemos alterar o comportamento de propagação da transação:
transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
Ou podemos definir um tempo limite, em segundos, para a transação:
transactionTemplate.setTimeout(1000);
É até possível se beneficiar de otimizações para transações somente leitura:
transactionTemplate.setReadOnly(true);
De qualquer forma, assim que criarmos umTransactionTemplate com uma configuração, todas as transações usarão essa configuração para executar. Portanto,if we need multiple configurations, we should create multiple template instances.
4. UsandoPlatformTransactionManager
Além deTransactionTemplate, , swe pode usar uma API de nível ainda mais baixo comoPlatformTransactionManager para gerenciar transações manualmente. Curiosamente, ambos@Transactional eTransactionTemplate usam essa API para gerenciar suas transações internamente.
4.1. Configurando transações
Antes de usar essa API, devemos definir a aparência da nossa transação. Por exemplo, podemos definir um tempo limite de três segundos com o nível de isolamento de transação de leitura repetível:
DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
definition.setIsolationLevel(TransactionDefinition.ISOLATION_REPEATABLE_READ);
definition.setTimeout(3);
As definições de transação são semelhantes às configurações deTransactionTemplate . No entanto, we can use multiple definitions with just one *PlatformTransactionManager*.
4.2. Atualização de transações
Após configurar nossa transação, podemos gerenciar programaticamente as transações:
@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. Conclusão
Neste tutorial, primeiro, vimos quando alguém deve escolher o gerenciamento programático de transações em vez da abordagem declarativa. Em seguida, introduzindo duas APIs diferentes, aprendemos como criar, confirmar ou reverter manualmente qualquer transação.
Como de costume, o código de amostra está disponívelover on GitHub.