No Spring, readOnly não é proteção
readOnly não transforma método em área segura.
Muita gente olha @Transactional(readOnly = true) e entende aquilo como uma trava confiável contra alteração de dados.
E, quando quer reforçar a ideia de "consulta pura", ainda empilha Propagation.NEVER por cima.
As duas leituras erram pelo mesmo motivo: tratam configuração transacional como se ela fosse política de negócio.
No Spring, não é isso que essas opções fazem.
readOnly é dica, não bloqueio
No geral, readOnly funciona como hint para a infraestrutura transacional e para o provider de persistência.
Ele pode comunicar intenção semântica e, em alguns cenários, ajudar em otimizações.
Mas isso é bem diferente de dizer que o método ficou tecnicamente impedido de escrever.
Um código como este mostra por que a leitura fica perigosa:
@Transactional(readOnly = true)
public void atualizarCadastro(Long clienteId, String novoEmail) {
Cliente cliente = clienteRepository.findById(clienteId)
.orElseThrow();
cliente.alterarEmail(novoEmail);
clienteRepository.save(cliente);
}
A anotação pode passar a sensação de "aqui ninguém escreve".
Só que readOnly não é mecanismo de autorização, não é trava de domínio e não é cerca de proteção contra código de escrita.
O ponto nem é discutir aqui a reação exata de cada stack ou provider diante desse fluxo.
O ponto é mais simples:
se o time quer impedir alteração, precisa modelar essa garantia na regra, na arquitetura ou no banco.
Depositar essa responsabilidade na anotação é inventar uma proteção que ela não prometeu.
O erro irmão: usar NEVER como padrão de leitura
Outro tropeço comum é decidir que toda consulta deveria rodar com Propagation.NEVER.
A ideia costuma soar elegante:
"leitura boa é leitura fora de transação."
Só que NEVER não significa "modo otimizado de leitura".
Significa: esse método falha se for chamado dentro de uma transação existente.
Um exemplo simples deixa isso mais claro:
@Service
public class FechamentoFaturaService {
private final ClienteQueryService clienteQueryService;
private final FaturaRepository faturaRepository;
public FechamentoFaturaService(
ClienteQueryService clienteQueryService,
FaturaRepository faturaRepository
) {
this.clienteQueryService = clienteQueryService;
this.faturaRepository = faturaRepository;
}
@Transactional
public void fechar(Long faturaId) {
Fatura fatura = faturaRepository.findById(faturaId)
.orElseThrow();
ClientePerfil perfil = clienteQueryService.buscarPerfil(fatura.getClienteId());
fatura.fecharCom(perfil);
}
}
@Service
public class ClienteQueryService {
@Transactional(readOnly = true, propagation = Propagation.NEVER)
public ClientePerfil buscarPerfil(Long clienteId) {
return clienteRepository.buscarPerfil(clienteId);
}
}
Na superfície, parece que buscarPerfil() está "protegido" como leitura.
Na prática, ele virou um ponto que explode sempre que um fluxo transacional legítimo precisa consultar algo no meio do caminho.
O método nem executa.
Ele falha antes da consulta porque a regra configurada não era "leitura segura".
Era "não aceito transação ativa".
Esse ponto conversa diretamente com o post sobre propagation: o nome parece explicar a intenção, mas o que vale é a semântica operacional.
O erro real é inventar semântica em cima da anotação
É isso que conecta os dois casos.
readOnly parece mais forte do que realmente é.
NEVER parece mais inocente do que realmente é.
Nos dois cenários, o erro nasce da mesma fonte: olhar para a configuração e imaginar uma semântica mais confortável do que a real.
readOnly não quer dizer "ninguém escreve aqui".
NEVER não quer dizer "consulta saudável".
Uma opção fala sobre intenção de leitura e possíveis otimizações.
A outra fala sobre falhar quando existe transação ativa.
Nenhuma delas substitui desenho transacional consciente.
O ponto que vale fixar
Se a equipe quer garantir que um fluxo não escreva, essa garantia precisa existir em um lugar mais sólido do que a anotação.
Se a equipe quer que uma consulta possa ser chamada dentro e fora de transação, precisa escolher propagation com base nesse contrato real, não em estética de leitura.
No Spring, configuração transacional ajuda muito.
Mas ela não corrige interpretação errada do time.
