Capítulo 08: Migrações de Banco com Flyway e Gestão de Transações (@Transactional)
Especialização em Backend com Java & Spring Boot • Spring Boot 3 & Java 21 LTS • Spring Data JPA, Security, Microsserviços e Cloud
🗺️ Mapa Conceitual do Tópico
flowchart TD
A["Cliente HTTP / Frontend"] --> B["API Gateway / Router"]
B --> C["Controller / Handler"]
C --> D["Service Layer (Regras de Negócio)"]
D --> E["Repository / ORM (Persistência)"]
E --> F["Banco de Dados / Cache"]
subgraph ARQ["Arquitetura do Capítulo"]
G["Conceito: Migrações de Banco com Flyway e Gestão de Transações (@Transactional)"]
H["Segurança, Validação e Resiliência"]
I["Alta Performance e Escalabilidade"]
end
D --> ARQ
style A fill:#e1f5fe,stroke:#03a9f4,stroke-width:2px
style B fill:#fff3e0,stroke:#ff9800,stroke-width:2px
style C fill:#ede7f6,stroke:#7e57c2,stroke-width:2px
style D fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
style E fill:#fce4ec,stroke:#e91e63,stroke-width:2px
style F fill:#f3e5f5,stroke:#9c27b0,stroke-width:2px
🏛️ 1. Fundamentos Técnicos de Migrações de Banco com Flyway e Gestão de Transações (@Transactional)
Por que não usar spring.jpa.hibernate.ddl-auto=update em produção. Deixar o Hibernate inferir e alterar o schema automaticamente é conveniente em protótipos, mas perigoso em produção: não há histórico auditável de mudanças, não há como revisar em Pull Request o que vai mudar no banco, e o Hibernate pode tomar decisões destrutivas (dropar uma coluna renomeada, por exemplo) sem aviso. O Flyway resolve isso tratando o schema como código versionado.
Convenção de nomenclatura do Flyway. Cada script fica em src/main/resources/db/migration, nomeado como V<versão>__<descrição>.sql (dois underscores). Ao subir a aplicação, o Flyway compara os scripts existentes com a tabela de controle flyway_schema_history: qualquer V ainda não registrado é executado, em ordem, dentro de uma transação (quando o banco suporta DDL transacional). Cada execução grava um checksum do arquivo — scripts já aplicados nunca devem ser editados, pois isso quebra a validação de integridade (Migration checksum mismatch) na próxima inicialização; a correção correta é criar uma nova versão (V2__corrige_x.sql).
Migrações repetíveis (R__). Para objetos que podem ser recriados livremente (views, procedures), o Flyway suporta o prefixo R__<descrição>.sql, reexecutado sempre que seu conteúdo mudar, independentemente de ordem de versão.
@Transactional e o proxy do Spring. A anotação não executa a transação “no método” — o Spring cria um proxy (AOP) ao redor do bean; quando um cliente externo chama o método, o proxy abre a transação antes e faz commit/rollback depois. Isso implica uma armadilha clássica: chamadas internas (self-invocation) — um método do mesmo bean chamando outro método @Transactional via this.metodo() — não passam pelo proxy e a anotação é silenciosamente ignorada.
Rollback padrão. Por padrão, o Spring reverte a transação apenas para RuntimeException e Error — exceções checadas (Exception verificada, ex. IOException) não disparam rollback automaticamente, a menos que se declare rollbackFor = Exception.class explicitamente.
Propagation. Define como um método transacional se comporta ao ser chamado de dentro de outra transação já ativa. REQUIRED (padrão) participa da transação existente ou cria uma nova se não houver. REQUIRES_NEW sempre abre uma transação independente, suspendendo a atual — útil para logs de auditoria que devem persistir mesmo que a operação principal sofra rollback. NESTED cria um savepoint dentro da transação pai, permitindo rollback parcial.
Isolation. Controla o quanto uma transação enxerga alterações concorrentes de outras. READ_COMMITTED (padrão do PostgreSQL) evita dirty reads (ler dado não commitado); REPEATABLE_READ também evita non-repeatable reads; SERIALIZABLE é o mais restritivo, evitando phantom reads ao custo de mais bloqueios e menor concorrência.
readOnly = true. Em métodos de consulta, sinaliza ao Hibernate para não fazer dirty checking nas entidades carregadas (economia de CPU/memória) e permite a drivers/roteadores de banco direcionar a query para réplicas de leitura.
💻 2. Código de Demonstração Corporativo
-- src/main/resources/db/migration/V1__criar_tabela_pedidos.sql
CREATE TABLE pedidos (
id BIGSERIAL PRIMARY KEY,
status VARCHAR(20) NOT NULL DEFAULT 'CRIADO',
criado_em TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- src/main/resources/db/migration/V2__adicionar_coluna_total_pedidos.sql
ALTER TABLE pedidos ADD COLUMN total NUMERIC(10,2) NOT NULL DEFAULT 0;
package com.empresa.api.service;
import com.empresa.api.repository.ContaBancariaRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
@Service
public class TransferenciaService {
private final ContaBancariaRepository contaRepository;
private final AuditoriaService auditoriaService;
public TransferenciaService(ContaBancariaRepository contaRepository, AuditoriaService auditoriaService) {
this.contaRepository = contaRepository;
this.auditoriaService = auditoriaService;
}
// Isolamento explícito + rollback garantido para qualquer exceção (checada ou não).
@Transactional(isolation = Isolation.READ_COMMITTED, rollbackFor = Exception.class)
public void transferir(Long origemId, Long destinoId, BigDecimal valor) {
var origem = contaRepository.findById(origemId).orElseThrow();
var destino = contaRepository.findById(destinoId).orElseThrow();
origem.debitar(valor);
destino.creditar(valor);
// REQUIRES_NEW: o log de auditoria é persistido mesmo que esta transação
// sofra rollback logo em seguida por alguma validação posterior.
auditoriaService.registrar("Transferência de " + valor + " entre contas " + origemId + " e " + destinoId);
}
}
package com.empresa.api.service;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
@Service
public class AuditoriaService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void registrar(String evento) {
// Executa em transação própria, isolada da transação chamadora.
System.out.println("AUDITORIA: " + evento);
}
}