Pular para o conteúdo principal

Capítulo 2: Gerenciamento de Transações (@Transactional)

Introdução​

Imagine uma transferência bancária: você debita R$100 da conta A e credita R$100 na conta B. O que acontece se o crédito falhar após o débito? Sem transação, os R$100 desaparecem!

As transações garantem que suas operações de dados sejam confiáveis e consistentes.


🎯 Objetivos do Capítulo​

Ao final deste capítulo, você saberá:

  1. ✅ Compreender as propriedades ACID
  2. ✅ Utilizar @Transactional corretamente
  3. ✅ Escolher o nível de propagação adequado
  4. ✅ Compreender os níveis de isolamento
  5. ✅ Gerenciar rollbacks corretamente
  6. ✅ Evitar armadilhas comuns

1. O que é uma Transação?​

Definição​

Uma transação é um conjunto de operações que devem todas ter sucesso ou todas falhar.

Exemplo Concreto: Transferência Bancária​

// SEM transação (PERIGOSO!)
public void transferencia(Long contaA, Long contaB, BigDecimal valor) {
contaRepo.debitar(contaA, valor); // ← Sucesso: -100

// 💥 ERRO AQUI (rede, exceção, etc.)

contaRepo.creditar(contaB, valor); // ← Nunca executado!

// Resultado: R$100 DESAPARECERAM! 💀
}
// COM transação (SEGURO)
@Transactional
public void transferencia(Long contaA, Long contaB, BigDecimal valor) {
contaRepo.debitar(contaA, valor); // ← Dentro da transação

// 💥 ERRO AQUI

contaRepo.creditar(contaB, valor); // ← Nunca executado

// Resultado: ROLLBACK automático, o débito é DESFEITO ✓
}

2. As Propriedades ACID​

Cada transação respeita 4 propriedades fundamentais:

A - Atomicidade (Atomicity)​

"Tudo ou nada"

✅ Todas as operações têm sucesso → COMMIT
❌ Uma operação falha → ROLLBACK de TUDO

C - Consistência (Consistency)​

"O banco de dados permanece em um estado válido"

Antes: Conta A = R$500, Conta B = R$300, Total = R$800
Depois: Conta A = R$400, Conta B = R$400, Total = R$800

O dinheiro não desaparece nem aparece magicamente.

I - Isolamento​

"As transações não enxergam umas às outras"

Transação 1: Lê saldo = R$500
Transação 2: Modifica saldo = R$400
Transação 1: Lê novamente = R$500 (ou R$400 dependendo do isolamento)

D - Durabilidade (Durability)​

"Uma vez COMMIT, é definitivo"

Transação COMMIT ✓
→ Mesmo se o servidor cair depois, os dados estão salvos.

3. @Transactional em Detalhes​

Uso Básico​

import org.springframework.transaction.annotation.Transactional;

@Service
public class OrderService {

@Transactional
public void createOrder(Order order) {
// Todas essas operações estão em UMA transação
orderRepo.save(order);
inventoryService.decreaseStock(order.getItems());
paymentService.processPayment(order.getTotal());
emailService.sendConfirmation(order);
}
}

Como Funciona?​

O Spring utiliza um proxy ao redor da sua classe:

Onde Colocar @Transactional?​

// ✅ RECOMENDADO: No método de serviço
@Service
public class UserService {

@Transactional
public void createUser(User user) {
// ...
}
}

// ✅ OK: Na classe (todos os métodos públicos)
@Service
@Transactional
public class UserService {

public void createUser(User user) { /* transacional */ }
public void updateUser(User user) { /* transacional */ }
}

// ❌ NÃO FUNCIONA: Em métodos privados
@Service
public class UserService {

@Transactional // ← IGNORADO!
private void internalOperation() {
// O proxy não pode interceptar métodos privados
}
}

4. Níveis de Propagação​

A propagação define como uma transação se comporta quando chama outro método transacional.

REQUIRED (Padrão)​

"Reutiliza a transação existente, ou cria uma nova"

@Transactional(propagation = Propagation.REQUIRED)  // Padrão
public void methodA() {
methodB(); // ← Reutiliza a transação de methodA
}

@Transactional
public void methodB() {
// Mesma transação que methodA
}
methodA() BEGIN TRANSACTION
├── operações de methodA
├── methodB() [mesma transação]
│ └── operações de methodB
└── COMMIT (ou ROLLBACK global)

REQUIRES_NEW​

"Sempre cria uma nova transação"

@Transactional
public void methodA() {
// Transação 1
repo.save(entity);

loggingService.logAction("action"); // ← Transação 2 (independente)

throw new RuntimeException(); // ← Rollback Transação 1
// Mas o log é MANTIDO (Transação 2 commitada)
}

@Service
public class LoggingService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void logAction(String action) {
// Nova transação independente
logRepo.save(new Log(action));
}
}
methodA() BEGIN TRANSACTION 1
├── save entity
├── logAction() BEGIN TRANSACTION 2
│ ├── save log
│ └── COMMIT TRANSACTION 2 ✓
├── throw exception
└── ROLLBACK TRANSACTION 1 (entity desfeito, log mantido)

Outras Propagações​

PropagaçãoDescrição
REQUIREDReutiliza ou cria (padrão)
REQUIRES_NEWSempre nova transação
SUPPORTSTransação se já existir, senão não transacional
MANDATORYTransação obrigatória (exceção se não houver)
NOT_SUPPORTEDSuspende a transação existente
NEVERExceção se houver transação existente
NESTEDTransação aninhada (savepoints)

5. Níveis de Isolamento​

O isolamento define como as transações enxergam as modificações de outras transações.

Problemas de Isolamento​

1. Dirty Read (Leitura Suja)​

Transação A: UPDATE saldo = 400 (ainda não COMMIT)
Transação B: SELECT saldo → 400 (leu um valor não commitado!)
Transação A: ROLLBACK
Resultado: B leu um valor que não existe mais

2. Non-Repeatable Read (Leitura Não Repetível)​

Transação A: SELECT saldo → 500
Transação B: UPDATE saldo = 400, COMMIT
Transação A: SELECT saldo → 400 (valor diferente!)

3. Phantom Read (Leitura Fantasma)​

Transação A: SELECT COUNT(*) WHERE status='ACTIVE' → 10
Transação B: INSERT INTO users (status='ACTIVE'), COMMIT
Transação A: SELECT COUNT(*) WHERE status='ACTIVE' → 11 (novo registro!)

Níveis de Isolamento​

NívelDirty ReadNon-RepeatablePhantom
READ_UNCOMMITTED❌ Possível❌ Possível❌ Possível
READ_COMMITTED✅ Impedido❌ Possível❌ Possível
REPEATABLE_READ✅ Impedido✅ Impedido❌ Possível
SERIALIZABLE✅ Impedido✅ Impedido✅ Impedido

Utilização​

// Padrão: READ_COMMITTED (depende do banco de dados)
@Transactional
public void operacaoNormal() { }

// Para relatórios financeiros: SERIALIZABLE
@Transactional(isolation = Isolation.SERIALIZABLE)
public BigDecimal calcularSaldoTotal() { }

// Para leituras simples: READ_COMMITTED é suficiente
@Transactional(isolation = Isolation.READ_COMMITTED)
public User getUser(Long id) { }
cuidado

SERIALIZABLE é o mais seguro, mas o mais lento. Use apenas quando realmente necessário!


6. Rollback e Exceções​

Comportamento Padrão​

@Transactional
public void operacao() {
repo.save(entity);

throw new RuntimeException(); // ✅ ROLLBACK automático
}

@Transactional
public void operacao() throws IOException {
repo.save(entity);

throw new IOException(); // ❌ NÃO faz rollback! (checked exception)
}

Regra padrão:

  • ✅ RuntimeException (unchecked) → Rollback
  • ❌ Exception (checked) → Não faz rollback

Forçar o Rollback​

// Rollback em todas as exceções
@Transactional(rollbackFor = Exception.class)
public void operacao() throws Exception {
repo.save(entity);
throw new IOException(); // ✅ ROLLBACK agora
}

// Rollback em exceções específicas
@Transactional(rollbackFor = {IOException.class, CustomException.class})
public void operacao() throws Exception { }

Não Fazer Rollback​

// Não faz rollback em certas exceções
@Transactional(noRollbackFor = BusinessException.class)
public void operacao() {
repo.save(entity);
throw new BusinessException("Regra de negócio violada");
// Não faz rollback, a entidade é salva
}

7. Transações Somente Leitura​

Otimização para Leituras​

@Transactional(readOnly = true)
public List<User> getAllUsers() {
return userRepo.findAll();
}

@Transactional(readOnly = true)
public User getUser(Long id) {
return userRepo.findById(id).orElseThrow();
}

Vantagens:

  • O Hibernate desativa o dirty checking (mais rápido)
  • Alguns bancos de dados otimizam queries read-only
  • Documentação explícita da intenção

No Nível da Classe​

@Service
@Transactional(readOnly = true) // Padrão: somente leitura
public class UserService {

public List<User> getAllUsers() { } // readOnly = true
public User getUser(Long id) { } // readOnly = true

@Transactional // Override: leitura e escrita
public void createUser(User user) { }
}

8. Timeout​

Limitar a Duração de uma Transação​

@Transactional(timeout = 5)  // Máximo 5 segundos
public void operacaoLonga() {
// Se levar mais de 5 segundos → TransactionTimedOutException
}

Casos de uso:

  • Consultas complexas que podem travar
  • Proteção contra deadlocks
  • Garantir a responsividade do sistema

9. Armadilhas Comuns​

❌ Armadilha 1: Self-Invocation (A Mais Frequente!)​

@Service
public class UserService {

public void methodA() {
this.methodB(); // ❌ @Transactional IGNORADO!
}

@Transactional
public void methodB() {
// Sem transação porque foi chamado da mesma classe
}
}

Por quê? O proxy só pode interceptar chamadas externas.

Solução:

@Service
public class UserService {

@Autowired
private UserService self; // Injeção de si mesmo

public void methodA() {
self.methodB(); // ✅ Passa pelo proxy
}

@Transactional
public void methodB() {
// Transação ativa!
}
}

❌ Armadilha 2: Exceção Engolida​

@Transactional
public void operacao() {
try {
riskyOperation();
} catch (Exception e) {
log.error("Erro ignorado", e); // ❌ Exceção engolida
// A transação não sabe que houve erro!
}
}

Solução:

@Transactional
public void operacao() {
try {
riskyOperation();
} catch (Exception e) {
log.error("Erro", e);
throw e; // ✅ Re-lança para disparar o rollback
}
}

❌ Armadilha 3: @Transactional na Interface​

// ❌ NÃO FUNCIONA com proxies CGLIB (padrão)
public interface UserService {
@Transactional
void createUser(User user);
}

// ✅ Coloque a anotação na implementação
@Service
public class UserServiceImpl implements UserService {
@Transactional
public void createUser(User user) { }
}

❌ Armadilha 4: Método Final​

@Service
public class UserService {

@Transactional
public final void operacao() { // ❌ CGLIB não pode criar proxy
// Transação ignorada!
}
}

Solução: Remova a palavra-chave final.


10. Resumo​

Recapitulação dos Atributos​

@Transactional(
propagation = Propagation.REQUIRED, // Comportamento da transação
isolation = Isolation.READ_COMMITTED, // Nível de isolamento
timeout = 30, // Timeout em segundos
readOnly = false, // Somente leitura?
rollbackFor = Exception.class, // Rollback para quais exceções?
noRollbackFor = BusinessException.class // Não faz rollback para quais exceções?
)
public void operacao() { }

Configurações Comuns​

// Criação/Alteração padrão
@Transactional
public void createUser(User user) { }

// Leitura otimizada
@Transactional(readOnly = true)
public User getUser(Long id) { }

// Operação crítica (auditoria)
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void auditLog(String action) { }

// Processamento de pagamento
@Transactional(
isolation = Isolation.SERIALIZABLE,
rollbackFor = Exception.class
)
public void processPayment(Payment payment) { }

Checklist​

✅ Coloque @Transactional nos métodos de serviço (não no controller)
✅ Use readOnly = true para leituras
✅ Entenda a propagação utilizada
✅ Verifique se as exceções disparam rollback
✅ Evite self-invocation
✅ Não engula exceções

🎯 Próximo Passo​

Agora que você domina transações, vamos aprender a versionar seu schema de banco de dados com Flyway.

➡️ Capítulo 3: Migração de Schema com Flyway


⬅️ Voltar ao Módulo 7