0022 — Persistência por Micronaut Data: o critério lote × CRUD¶
Status: Aprovado · Responsável: Gustavo Madruga · Atualizado em: 2026-09-15 · Decidido em: 2026-09-11
Contexto¶
O int-pied nasceu com a persistência inteira em SQL cru: repositórios @Singleton abrindo
dataSource.getConnection() e montando o PreparedStatement à mão — 58 chamadas em 16 classes. A
decisão 0012 trouxe o micronaut-data-jdbc à força
(o outbox do xadm-mensageria é @JdbcRepository) e registrou o app como "raw-SQL por design": o
Data entrou para o @Transactional funcionar, e o SQL continuou à mão.
A norma da casa fechou essa porta depois
(engenharia/java-micronaut §Banco):
micronaut-data-jdbc é obrigatório para server que persiste, e SQL cru via
DataSource.getConnection() como camada de persistência é anti-padrão. O que segue legítimo à mão é
o trabalho de lote num @Singleton — o veto é à persistência inteira em SQL cru.
Decisão¶
Toda persistência é Micronaut Data; SQL à mão só onde é trabalho de conexão de verdade, numa allowlist guardada por teste. O critério por repositório:
| Tipo de acesso | Forma | Onde |
|---|---|---|
| CRUD puro | @JdbcRepository sobre CrudRepository, entidade @MappedEntity, métodos derivados + @Query pontual |
DestinatarioAlertaRepository, WatchdogEstadoRepository |
| Comando, upsert, projeção | @Query nativo com o SQL de antes, numa interface @JdbcRepository aninhada (Sql) |
PiedPedidoRepository, CursorRepository, NormalizacaoEstadoRepository, PiedClienteRepository, PiedProdutoRepository, PiedWebhookRepository, PiedRestRepository, PainelRepository, DadosRepository, CapturaRepository |
| Poda por idade | @Query nativo, um DELETE por tabela |
RetencaoRepository |
| Lote / sessão (manual) | DataSource.getConnection() |
NormalizadorService (varredura do raw), PiedClusterLock (advisory lock) |
- A classe do repositório fica; o SQL vai para a interface aninhada. Nos repositórios de comando e
projeção, a classe
@Singletonmantém a API de domínio (osbooleande "fez a transição?", os defaults de "sem linha", o envelope JSON do webhook, o pretty-print da captura), a transação REQUIRED por método (@Transactionalna classe, como antes) e o construtor que os fakes dos testes unitários usam (super(null)). OSqlé o@JdbcRepository. Repositório sem nada disso a preservar é a própria interface (RetencaoRepository, e os dois de CRUD). - Muda o transporte, não a semântica. O texto SQL é o mesmo:
?virou:nomee?::jsonbvirouCAST(:x AS jsonb). Nopied_pedidoisso inclui o upsert com os quatroCASEde transição e a guarda de recência, e as transições guardadas porstatusnoWHERE. Nuncasave()/update()de entidade nopied_pedido: oUPDATEgerado reescreveria as colunas da máquina de entrega e as guardas deixariam de existir. - A chave e os carimbos seguem com o banco. Insert por
@Queryonde osave()exigiria gerar ouuidno app;uuidv7(),criado_em/recebido_em/buscado_emeatualizado_em = now()como antes. Ojsonbcontinua gravado como texto convertido pelo banco — o mesmo dado. - Sai o código sem chamador:
IntegracaoEstadoRepository,PiedPedidoRepository.enfileirarElegiveis(o corte go-forward perdeu o papel na revisão do gate de 2026-08-07) ePiedClienteRepository.inscEstDe(lido só por teste; a leitura foi para aBancoFixture, decisão 0013). A tabelapied_integracao_estadofica: só o/dados/estadoa lê.
Correção (2026-09-15): a varredura do raw do
NormalizadorServicesai da allowlist — o motivo "lote / sessão" não corresponde ao código. OPreparedStatementda varredura não chamasetFetchSize, e sem ele o PgJDBC traz oResultSetinteiro para a memória antes da primeira linha: não há streaming a preservar, é umSELECTcomum aberto à mão. O texto SQL segue o mesmo, agora@Querynativo noRawCapturadoRepositorydevolvendo a projeção@Introspected RawLinha(ts,fonte,tipo,payload), com o recorte incremental num parâmetro nomeado único (CAST(:desde AS timestamptz), o mesmo valor testado contraNULLe comparado nas duas fontes). A allowlist fica só com oPiedClusterLock, cujo advisory lock de sessão precisa mesmo da conexão. O consumo de memória não muda de ordem: o recorte pelo cursor (V11) é que mantém a leitura pequena.
A trava¶
FronteirasTest.persistenciaSoPorMicronautData: nenhuma classe fora da allowlist chama
DataSource.getConnection() (casa por nome e por dono atribuível a DataSource, então pega também a
chamada por um subtipo). Provada com mutante: uma classe nova em dados chamando getConnection()
deixa o teste vermelho, apontando a linha; sem ela, verde. Pôr uma classe na allowlist é decisão, com o
motivo no comentário ao lado — não atalho.
O que o Micronaut Data 5.0.4 cobrou¶
Achados da migração, todos verificados no build:
- Todo
@JdbcRepositoryprecisa de entidade-raiz, mesmo só com@Query: semGenericRepository<E, ID>o processor falha com Persistent entity is required. Nos repositórios de SQL nativo a raiz é umrecord Raiz(@Id …)que mapeia só a chave. - O mapeador de projeção lê a propriedade homônima da raiz antes da do record. Por isso o
DadosRepositorye oCapturaRepository, cujas linhas têmidde tipos diversos (uuid como texto,intdo singleton), usam uma raiz chaveada porentidade, não porid. - Record de projeção precisa de
@Introspectede de@Nullableonde a coluna pode vir nula — sem a anotação o mapeador recusa o null ao construir o record. O mapeamento passou a ser por nome de coluna (antes era por posição): onde o rótulo diferia do componente, entrou um alias (documento_cliente AS documento). @Queryexige constante de compilação, e três leituras montavam o SQL em runtime. Viraram variantes finitas: no painel, um método por bucket com o predicado literal — e oStatusBucketTesttrava cada literal contra oStatusBucket.sqlFiltro(), para bucket novo não divergir em silêncio —; na timeline da captura, uma por fonte; na retenção, umDELETEpor tabela.- Parâmetro nomeado é só
[a-zA-Z0-9](sem_), e o::do Postgres saiu do texto (CAST(… AS …)) para o SQL não depender do parser de parâmetros. - Falha de banco chega como
DataAccessException, não mais como oIllegalStateExceptionque os repositórios embrulhavam. Os chamadores capturamRuntimeException/Exception, então o comportamento não muda. - Native: o Micronaut Data gera introspecção e consultas em compilação, sem reflexão — nenhum hint a acrescentar.
Alternativas descartadas¶
- Repositório como classe abstrata
@JdbcRepository(API pública concreta,@Queryabstrato). Funciona, mas os fakes dos testes unitários (PushEnviadorTest,PollServiceTest) herdam os repositórios e teriam de implementar cada@Queryabstrato — cerca de vinte só noPiedPedidoRepository, e mais um a cada consulta nova. - A interface
@JdbcRepositorycomo o próprio repositório, com a lógica em métodosdefault. O mesmo problema dos fakes. - Entidade completa +
save()/update(). Reescreveria o SQL que carrega regra de negócio (FSM guardada noWHERE,COALESCE/GREATEST,now()do banco): mudaria a semântica, não só o transporte.
Consequência¶
A decisão 0012 deixa de ser "raw-SQL por design" (ver a
emenda de 2026-09-11 nela). A suíte de testes atravessou as cinco fatias da migração sem mudança, salvo a
leitura do state_inscription (foi para a BancoFixture) e a API da retenção.