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á:
- ✅ Compreender as propriedades ACID
- ✅ Utilizar @Transactional corretamente
- ✅ Escolher o nível de propagação adequado
- ✅ Compreender os níveis de isolamento
- ✅ Gerenciar rollbacks corretamente
- ✅ 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ção | Descrição |
|---|---|
| REQUIRED | Reutiliza ou cria (padrão) |
| REQUIRES_NEW | Sempre nova transação |
| SUPPORTS | Transação se já existir, senão não transacional |
| MANDATORY | Transação obrigatória (exceção se não houver) |
| NOT_SUPPORTED | Suspende a transação existente |
| NEVER | Exceção se houver transação existente |
| NESTED | Transaçã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ível | Dirty Read | Non-Repeatable | Phantom |
|---|---|---|---|
| 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) { }
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