No Spring, Propagation não significa o que você acha
@Transactional muitas vezes não faz o que o nome sugere.
Boa parte da confusão vem da enumeração de Propagation.
Você lê REQUIRED, REQUIRES_NEW, SUPPORTS, MANDATORY, NOT_SUPPORTED, NEVER e parece que o comportamento já está óbvio.
Só que esses nomes são muito mais perigosos do que parecem quando a leitura fica superficial.
O primeiro erro: ler o nome e imaginar a semântica
Muita gente trata propagation como se fosse uma preferência declarativa.
"Aqui eu quero uma transação nova."
"Aqui tanto faz."
"Aqui é só leitura."
Mas propagation não é intenção solta.
É regra de execução.
E essa regra só entra em cena quando a chamada foi realmente interceptada.
É exatamente o ponto do post sobre @Transactional e proxy: antes de discutir o valor da propagation, você precisa ter certeza de que o método passou pela fronteira onde o Spring consegue agir.
O que os principais valores realmente fazem
Antes de entrar nos mal-entendidos mais comuns, vale traduzir a enumeração para semântica de execução:
REQUIRED: entra na transação atual; se não existir, abre umaREQUIRES_NEW: suspende a transação atual e abre outraSUPPORTS: participa da atual se existir; senão, executa sem transaçãoMANDATORY: exige que já exista uma transação ativaNOT_SUPPORTED: suspende a atual e executa fora de transaçãoNEVER: falha se existir transação ativaNESTED: tenta criar um escopo aninhado com savepoint, quando o ambiente suporta
O problema é que esses nomes parecem descrever intenção de negócio.
Mas eles descrevem apenas como o Spring deve reagir diante da presença ou ausência de transação ativa no momento da chamada.
REQUIRED não quer dizer "sempre transacional do mesmo jeito"
REQUIRED é o padrão.
E justamente por isso ele costuma ser lido de forma preguiçosa.
Muita gente assume que REQUIRED significa simplesmente "este método é transacional".
Só que a regra real é:
- se já existe transação, ele participa dela;
- se não existe, ele abre uma.
Na prática, o mesmo método pode:
- abrir sua própria transação quando chamado de fora;
- apenas entrar na transação de outro método quando chamado dentro de um fluxo já aberto.
Ou seja: até o valor mais comum muda de comportamento conforme o contexto.
REQUIRES_NEW não cria mágica por presença
REQUIRES_NEW significa:
se a chamada for interceptada, o Spring suspende a transação atual e abre outra.
Esse detalhe importa.
O que abre a nova transação não é o texto da anotação.
É a interceptação da chamada.
Por isso este código engana tanto:
@Transactional
public void processar(Long pedidoId) {
registrarAuditoria(pedidoId);
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void registrarAuditoria(Long pedidoId) {
auditoriaRepository.save(new Auditoria("PROCESSANDO", pedidoId));
}
Se registrarAuditoria() for chamado internamente na mesma classe, o proxy fica fora da conversa.
Nesse caso, o REQUIRES_NEW parece presente, mas não produz o comportamento que o time esperava.
SUPPORTS não é "tem transação, mas de boa"
SUPPORTS também é muito mal interpretado.
O nome faz muita gente imaginar algo como "participa quando dá, mas continua protegido".
Na prática, a regra é mais seca:
- se já existir transação, o método participa dela;
- se não existir, o método roda sem transação nenhuma.
Isso muda bastante a leitura do código.
O mesmo método pode estar transacional em um fluxo e completamente sem transação em outro.
Se o time olha apenas para a anotação e assume uma garantia uniforme, está lendo mais intenção do que semântica real.
E não: usar SUPPORTS em consulta não significa que ela virou readOnly.
Isso é outra configuração, e também costuma ser mal interpretada.
MANDATORY e NEVER são regras de contrato, não nuances
Esses dois costumam ser menos usados, mas são muito úteis para mostrar como os nomes podem enganar.
MANDATORY significa:
- se existir transação, entra nela;
- se não existir, lança exceção.
Ou seja: ele não "prefere" transação.
Ele exige que alguém acima já tenha aberto a fronteira correta.
Já NEVER faz o inverso:
- se não existir transação, executa;
- se existir, lança exceção.
Isso é importante em fluxos que realmente precisam garantir execução fora de contexto transacional.
O ponto é que nenhum dos dois comunica "estilo".
Eles comunicam restrição operacional.
NOT_SUPPORTED não é ausência inocente de anotação
Outro nome traiçoeiro é NOT_SUPPORTED.
Muita gente lê isso como se fosse equivalente a "não quero transação aqui".
Mas o comportamento é mais específico:
- se houver transação ativa, o Spring suspende;
- o método executa fora de transação;
- depois, a transação anterior pode ser retomada.
Isso é bem diferente de simplesmente não anotar o método.
Sem anotação, você não está declarando nada.
Com NOT_SUPPORTED, você está dizendo explicitamente que, mesmo em um fluxo transacional, este trecho precisa rodar fora dele.
NESTED quase sempre é mais perigoso do que parece
NESTED costuma soar como "subtransação".
Esse nome convida muita gente a imaginar independência parecida com REQUIRES_NEW.
Mas a semântica não é essa.
Em geral, NESTED trabalha com savepoints dentro da transação existente, quando a infraestrutura suporta esse comportamento.
Isso significa que ele:
- não é a mesma coisa que abrir uma transação completamente separada;
- depende do suporte concreto do transaction manager e do banco;
- pode não funcionar do jeito que o time imaginou em todos os ambientes.
Ou seja: além de depender da interceptação, ainda depende da infraestrutura.
O erro recorrente: ler nomes como intenção de negócio
É aí que a enumeração engana.
REQUIRES_NEW parece "quero isolar".
SUPPORTS parece "tanto faz".
MANDATORY parece "importante".
NEVER parece "jamais mexa nisso".
Mas o que esses valores descrevem não é o que você quer dizer para o domínio.
Eles descrevem como o framework deve se comportar na borda transacional quando a chamada chega até ele.
Se o time usa esses nomes como documentação autoexplicativa, começa a inferir garantia onde só existe regra técnica.
O que realmente importa
Antes de discutir qual propagation usar, duas perguntas vêm primeiro:
- Essa chamada atravessa o proxy do Spring?
- O time entende de forma precisa o que essa propagation faz quando há ou não há transação ativa?
Se a primeira resposta for não, o valor configurado sequer entra em ação.
Se a segunda resposta for vaga, a anotação vira uma promessa mal lida.
É por isso que propagation não deveria ser escolhida por nome bonito.
Ela deveria ser escolhida depois de responder perguntas mais concretas:
- esse trecho deve obrigatoriamente participar da transação atual?
- ele precisa abrir outra fronteira real?
- ele deve falhar se não houver transação?
- ele precisa garantir execução fora do contexto transacional?
- o ambiente suporta o comportamento esperado, especialmente em
NESTED?
O ponto que vale fixar
Propagation não é nome bonito para documentar intenção.
É comportamento de execução.
E esse comportamento só funciona quando a chamada é interceptada.
Antes de confiar no nome do enum, vale lembrar:
- ele não explica sozinho a semântica;
- ele não substitui desenho de fronteira;
- e ele não faz nada se a chamada não passou pelo proxy do Spring.
