Pular para o conteúdo principal

ADR-017: Acesso a Dados de Documentos — PostgreSQL JSONB

Status: Aceita
Data: 2026-06-27

Contexto

O stack precisa suportar dados semi-estruturados ou com schema flexível: payloads de eventos heterogêneos, configurações dinâmicas por tenant, metadados extensíveis em entidades relacionais. A abordagem tradicional seria introduzir um banco de documentos (MongoDB, CouchDB) como segundo serviço de backing. Essa decisão avalia se o PostgreSQL — já presente no stack (ADR-007) — pode cobrir esse caso de uso com qualidade suficiente.

Decisão

PostgreSQL JSONB via Mojo::Pg para todos os casos de uso de dados de documento. Nenhum banco de documentos separado é introduzido no stack.

Justificativa

O tipo JSONB do PostgreSQL armazena documentos JSON em formato binário indexável. Com índices GIN é possível executar queries eficientes sobre campos internos de documentos heterogêneos usando operadores nativos como @> (containment), #> (path), ? (key existence) e funções como jsonb_array_elements. A performance de queries JSONB com GIN indexes é comparável à de bancos de documentos dedicados para os padrões de acesso típicos de APIs web.

A decisão por JSONB elimina a necessidade de um segundo serviço de banco de dados no stack, com os seguintes benefícios diretos para a infraestrutura:

  • Um único container de banco no Docker Compose e no Kubernetes
  • Backup e restore unificados: um pg_dump cobre dados relacionais e documentais
  • Transactions ACID entre dados relacionais e documentais: é possível, em uma única transação, atualizar uma linha SQL e um documento JSONB na mesma operação
  • Sem driver adicional: Mojo::Pg (ADR-016) já lida com JSONB nativamente

Referências: PostgreSQL, Mojo::Pg, Mango (alternativa rejeitada), DocumentDB (alternativa rejeitada)

Schema: coluna JSONB em tabela existente

-- migrations/004_create_events.sql
-- (arquivo separado — ver ADR-016 sobre a convenção de múltiplos arquivos)

-- 4 up
-- Tabela de eventos com schema 100% flexível
CREATE TABLE events (
id BIGSERIAL PRIMARY KEY,
type TEXT NOT NULL,
payload JSONB NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX ON events (type);
CREATE INDEX ON events USING GIN (payload);

-- 4 down
DROP TABLE events;

Escrita de documentos

# Inserir evento com payload heterogêneo
$self->pg->db->query(
'INSERT INTO events (type, payload) VALUES (?, ?)',
'user.created',
{ json => { user_id => 42, plan => 'pro', source => 'signup_form' } }
);

# Atualizar campo JSONB (merge parcial)
$self->pg->db->query(
q{UPDATE posts SET metadata = metadata || ? WHERE id = ?},
{ json => { featured => \1, views => 0 } },
$post_id
);

O Mojo::Pg usa { json => ... } para serializar automaticamente estruturas Perl para JSONB, sem necessidade de encode_json manual.

Queries sobre documentos

# @> : containment — eventos do tipo user.created com plano 'pro'
my $events = $self->pg->db->query(
q{SELECT id, payload, created_at
FROM events
WHERE type = ? AND payload @> ?
ORDER BY created_at DESC
LIMIT 50},
'user.created',
{ json => { plan => 'pro' } }
)->expand->hashes;

# #> : path query — extrair campo aninhado
my $plans = $self->pg->db->query(
q{SELECT payload #>> '{subscription,plan}' AS plan, COUNT(*) AS total
FROM events
WHERE type = 'user.created'
GROUP BY plan}
)->hashes;

# ? : verificar existência de chave
my $featured = $self->pg->db->query(
q{SELECT id, title FROM posts WHERE metadata ? 'featured'}
)->hashes;

O método ->expand instrui o Mojo::Pg a deserializar automaticamente colunas JSONB para estruturas Perl (hashrefs e arrayrefs), sem decode_json manual.

Atualização atômica de campos

# jsonb_set: atualizar um campo sem sobrescrever o documento inteiro
$self->pg->db->query(
q{UPDATE events
SET payload = jsonb_set(payload, '{processed}', 'true')
WHERE id = ?},
$event_id
);

Quando usar JSONB vs. colunas SQL normais

SituaçãoAbordagem
Dados com schema fixo e conhecidoColunas SQL tipadas
Dados semi-estruturados com campos variáveisColuna JSONB com índice GIN
Payload de eventos (schema por tipo)Tabela de eventos com coluna JSONB
Configurações por tenant/usuárioColuna JSONB em tabela de configurações
Dados que precisam de JOIN relacionalColunas SQL normais (JSONB não faz JOIN eficiente)

Alternativas Consideradas

AlternativaMotivo da rejeição
MongoDB (driver oficial Perl)Segundo serviço de backing a operar; licença SSPL restritiva; driver oficial Perl é síncrono (bloqueia o event loop do Mojo)
DocumentDB + MangoDocumentDB é extensão nova (maturidade a verificar); Mango está abandonado (incompatível com Perl 5.38+); adiciona complexidade sem benefício sobre JSONB nativo
Colunas TEXT com JSON serializado manualmenteSem indexação, sem operadores nativos, sem validação de tipo — essencialmente o que o JSONB resolve, mas pior
CouchDBServiço separado; ecossistema Perl muito limitado; sem transações ACID

Consequências

Positivo:

  • Zero serviços adicionais: dados documentais no mesmo PostgreSQL dos dados relacionais
  • Transações ACID entre dados relacionais e documentais na mesma operação
  • Backup unificado: pg_dump cobre tudo
  • { json => ... } serializa Perl → JSONB na escrita; ->expand deserializa JSONB → Perl na leitura

Negativo:

  • JSONB não é otimizado para queries que percorrem arrays muito grandes de documentos sem discriminação por tipo/índice (caso de uso de analytics — fora do escopo)
  • Schema flexível requer disciplina da aplicação: o banco não rejeita documentos malformados (recomendado usar validação via JSON Schema ou CHECK CONSTRAINT)

Ações necessárias:

  • Criar índices GIN em todas as colunas JSONB que receberão queries por campos internos
  • Usar { json => ... } para escrita e ->expand para leitura com Mojo::Pg
  • Documentar as convenções de schema de cada tipo de evento em docs/references/ ou como JSON Schema inline nos guias