A Consulta Correta Começa Antes do Código
Hatched by Felipe Soares Barbosa Silveira (Felipebros)
Aug 06, 2026
12 min read
2 views
83%
Uma consulta pode estar perfeitamente correta para o banco de dados e completamente errada para o negócio. Esse é um dos paradoxos mais caros do desenvolvimento de software: a aplicação executa sem erros, os testes técnicos passam, mas o resultado contradiz aquilo que alguém realmente pediu.
A pergunta decisiva, portanto, não é apenas “esta consulta funciona?”. É outra: conseguimos explicar, em linguagem humana, por que esta consulta deve existir?
Quando colocamos lado a lado uma linguagem de especificação como Gherkin e um construtor de consultas como o Query Builder do Laravel, surge uma conexão poderosa. Um organiza o comportamento esperado em frases compreensíveis. O outro transforma intenções em operações precisas sobre dados. Entre os dois existe uma oportunidade frequentemente desperdiçada: fazer com que a consulta seja uma consequência visível da regra de negócio, e não um trecho isolado de código que apenas parece plausível.
O problema não é escrever consultas. É preservar intenções
Imagine uma plataforma de assinaturas. A regra diz:
Um cliente ativo pode visualizar seus pedidos pagos dos últimos trinta dias.
Essa frase contém várias decisões. “Cliente ativo” define uma condição. “Seus pedidos” define uma relação de pertencimento. “Pagos” define um estado. “Últimos trinta dias” define um intervalo temporal. Uma consulta precisa traduzir todas essas dimensões sem perder nenhuma delas.
Em Laravel, poderíamos escrever algo semelhante a:
$pedidos = DB::table('orders')
->where('customer_id', $customerId)
->where('status', 'paid')
->where('created_at', '>=', Carbon::now()->subDays(30))
->orderBy('created_at', 'desc')
->get();
O código é legível. Porém, ainda não sabemos se ele representa exatamente a regra. O cliente precisa estar ativo? A consulta verifica isso? “Últimos trinta dias” significa trinta dias corridos, ou o mês atual? O pedido pode ser considerado pago se tiver sido reembolsado depois? O código responde apenas às condições que foram explicitamente incluídas. Ele não revela as condições que foram esquecidas.
É aqui que uma especificação estruturada se torna mais do que documentação. Em Gherkin, a regra poderia assumir esta forma:
Funcionalidade: Visualização de pedidos recentes
Cenário: Cliente ativo consulta pedidos pagos recentes
Dado que existe um cliente ativo
E que esse cliente possui um pedido pago criado há dez dias
E que esse cliente possui um pedido pago criado há quarenta dias
Quando o cliente consulta seus pedidos recentes
Então o pedido criado há dez dias deve ser exibido
E o pedido criado há quarenta dias não deve ser exibido
A diferença é fundamental. A consulta mostra como filtrar. O cenário explicita o que precisa ser verdade. A primeira é uma implementação possível. O segundo é um contrato observável.
Código diz ao computador quais operações executar. Uma boa especificação diz às pessoas qual realidade essas operações devem preservar.
Essa distinção muda a maneira de revisar código. Em vez de perguntar apenas se o encadeamento de métodos parece idiomático, podemos comparar cada cláusula da consulta com uma parte da regra. Se o cenário menciona que o cliente deve estar ativo, mas o código não contém essa condição, há uma divergência concreta. Se o código adiciona um filtro que ninguém consegue justificar pela regra, há outra forma de risco: a implementação pode estar restringindo o sistema sem autorização.
Gherkin e Query Builder são linguagens de composição
À primeira vista, Gherkin e Query Builder pertencem a mundos diferentes. Um usa frases como “Dado”, “Quando” e “Então”. O outro usa métodos como where, join, groupBy e orderBy. Mas ambos são linguagens de composição. Eles permitem construir uma descrição complexa a partir de elementos menores, mantendo a estrutura da intenção visível.
Considere uma tela que exibe produtos disponíveis para venda. A regra pode ser descrita assim:
Funcionalidade: Catálogo público
Cenário: Exibir produtos comercializáveis
Dado que um produto está ativo
E que possui estoque disponível
E que pertence a uma categoria publicada
Quando uma pessoa acessa o catálogo
Então o produto deve aparecer na lista
A consulta correspondente talvez seja:
$produtos = DB::table('products')
->join('categories', 'categories.id', '=', 'products.category_id')
->where('products.active', true)
->where('products.stock', '>', 0)
->where('categories.published', true)
->select('products.id', 'products.name', 'products.price')
->get();
Cada parte do encadeamento tem uma função semântica. O join conecta duas entidades necessárias para a regra. Os where restringem o conjunto aos produtos que satisfazem as condições. O select define quais informações serão expostas. A composição não é apenas uma conveniência sintática. Ela oferece um mapa visual da decisão.
Ainda assim, existe uma armadilha. A aparência declarativa pode criar uma falsa sensação de segurança. Métodos encadeados são fáceis de ler, mas podem esconder ambiguidades. Um join comum elimina produtos sem categoria correspondente. Um leftJoin preserva linhas sem correspondência. A escolha entre eles não é um detalhe técnico neutro. Ela altera o significado do catálogo.
Da mesma forma, where('stock', '>', 0) não necessariamente representa “disponível para venda”. Um produto pode ter estoque, mas estar bloqueado para uma região específica, aguardando aprovação ou fora do período promocional. O Query Builder permite escrever a consulta. Gherkin força a equipe a perguntar se a regra foi definida com precisão suficiente para ser consultada.
Essa é a primeira grande síntese: especificações e consultas funcionam como camadas complementares de uma mesma linguagem de decisão. A especificação reduz ambiguidade entre pessoas. O construtor de consultas reduz ambiguidade na execução. A qualidade do sistema depende da continuidade entre essas duas camadas.
A lacuna perigosa entre “passou” e “está correto”
Um cenário automatizado pode passar e, mesmo assim, não proteger o comportamento mais importante. Isso ocorre quando os dados do teste são pobres, quando as afirmações são vagas ou quando a implementação do teste repete a lógica da implementação principal.
Veja um cenário superficial:
Cenário: Consultar pedidos
Quando o cliente consulta seus pedidos
Então a requisição deve ser concluída com sucesso
Uma consulta que retorna todos os pedidos do banco pode fazer esse cenário passar. A requisição terminou com sucesso, mas a privacidade do cliente foi violada. O teste verificou disponibilidade, não correção.
Um cenário mais forte cria contraste entre casos semelhantes:
Cenário: Não exibir pedidos de outro cliente
Dado que o cliente A possui um pedido pago
E que o cliente B possui um pedido pago
Quando o cliente A consulta seus pedidos
Então o pedido do cliente A deve ser exibido
E o pedido do cliente B não deve ser exibido
O contraste funciona como uma espécie de instrumento de medição. Se todos os registros do banco pertencerem ao mesmo cliente, uma consulta sem filtro de proprietário pode parecer correta. Se o conjunto de dados incluir casos que competem entre si, a falha se torna observável.
Podemos pensar em três níveis de precisão:
- Existência: algo foi retornado.
- Forma: o retorno tem os campos e a estrutura esperados.
- Semântica: o retorno contém exatamente os registros que a regra autoriza.
Muitos testes de integração param no primeiro nível. Muitos testes de consulta verificam o segundo. O valor real aparece no terceiro, porque é ali que se detectam filtros ausentes, junções indevidas, duplicações e interpretações erradas de datas.
A consulta também precisa ser avaliada pelas bordas. Se a regra diz “pedidos dos últimos trinta dias”, o que acontece com um pedido criado exatamente trinta dias atrás? E com um pedido criado trinta dias e um segundo depois? Se a aplicação usa horário local, mas o banco armazena UTC, qual relógio determina a fronteira?
Um conjunto robusto de cenários poderia incluir:
Cenário: Incluir pedido exatamente no limite do período
Dado que o relógio do sistema marca 15 de maio às 10 horas
E que o cliente possui um pedido criado em 15 de abril às 10 horas
Quando o cliente consulta seus pedidos recentes
Então o pedido deve ser exibido
Cenário: Excluir pedido fora do limite do período
Dado que o relógio do sistema marca 15 de maio às 10 horas
E que o cliente possui um pedido criado em 15 de abril às 9 horas, 59 minutos e 59 segundos
Quando o cliente consulta seus pedidos recentes
Então o pedido não deve ser exibido
O objetivo não é transformar cada detalhe em uma cerimônia interminável. É revelar onde uma palavra aparentemente simples, como “recente”, contém uma decisão operacional. Toda ambiguidade de negócio que não é resolvida na especificação reaparece mais tarde como uma condição escondida no código.
A consulta como prova, não como palpite
Uma maneira mais produtiva de projetar esse tipo de código é tratar a consulta como uma prova executável da regra. Não uma prova matemática completa, mas uma cadeia de evidências: cada parte da consulta deve justificar uma afirmação do cenário, e cada afirmação importante deve ter dados capazes de desafiar a consulta.
Esse modelo pode ser chamado de rastreabilidade em três movimentos:
1. Intenção
A equipe descreve o comportamento em termos observáveis. Não começa pela tabela, pela coluna ou pelo método disponível no framework. Começa pelo que uma pessoa autorizada deve ver, fazer ou receber.
“Usuário pode visualizar seus relatórios aprovados” é uma intenção. “Aplicar where status = approved na tabela reports” é uma decisão de implementação. Misturar as duas cedo demais faz a estrutura atual do banco parecer uma regra do negócio.
2. Tradução
A intenção é convertida em operações. Aqui entram o Query Builder, os relacionamentos, os agrupamentos, os parâmetros e as transações. A tradução deve ser explícita o bastante para que outra pessoa consiga apontar qual parte do código corresponde a cada condição.
Por exemplo:
$relatorios = DB::table('reports')
->where('reports.owner_id', $userId)
->where('reports.status', 'approved')
->whereNull('reports.deleted_at')
->orderByDesc('reports.approved_at')
->get();
Talvez deleted_at seja uma condição necessária. Talvez seja apenas uma convenção técnica. A revisão precisa distinguir as duas coisas. Quando uma condição não aparece na especificação, ela deve ser questionada, não aceita automaticamente como parte inevitável da consulta.
3. Contraste
A equipe cria exemplos que poderiam expor uma tradução incorreta. Um relatório do usuário errado. Um relatório pendente. Um relatório excluído. Um relatório aprovado no passado, mas sem data de aprovação. O cenário não deve apenas confirmar o caminho feliz. Deve colocar a implementação diante de alternativas plausíveis.
Esse terceiro movimento é o que impede a especificação de virar decoração. Um cenário útil não repete a consulta em linguagem natural. Ele cria uma situação na qual uma consulta errada produziria uma resposta diferente da esperada.
A melhor especificação não é a que descreve o código com mais detalhes. É a que torna uma interpretação errada impossível de esconder.
Essa abordagem também ajuda a separar problemas diferentes. Se o cenário está claro, mas a consulta é difícil de montar, pode existir um problema de modelo de dados. Se a consulta é simples, mas o cenário é ambíguo, o problema está na regra. Se ambos parecem claros, mas o teste é frágil, talvez falte controle sobre tempo, estado ou dados de preparação.
O custo invisível das consultas sem linguagem de negócio
Consultas tendem a se acumular. Uma condição é adicionada para corrigir um caso urgente. Outra é introduzida por uma tela nova. Um terceiro desenvolvedor acrescenta uma junção para resolver uma exceção. Meses depois, ninguém sabe se a consulta ainda representa uma regra coerente ou apenas a soma histórica de remendos.
Esse fenômeno pode ser descrito como entropia semântica. A estrutura do código continua válida, mas a relação entre código e intenção se deteriora. O banco executa. O framework responde. A equipe, porém, perde a capacidade de explicar por que cada condição existe.
O risco não é apenas manutenção difícil. Consultas sem uma regra explícita podem produzir quatro tipos de falha:
- Exclusão indevida: registros legítimos deixam de aparecer.
- Inclusão indevida: registros fora do escopo são exibidos.
- Duplicação: uma junção ou agrupamento retorna o mesmo objeto mais de uma vez.
- Instabilidade temporal: o resultado muda conforme horário, fuso, ordem ou estado concorrente.
A resposta não é colocar uma descrição extensa em um comentário ao lado do código. Comentários podem explicar uma decisão, mas não demonstram que a decisão continua verdadeira. O ideal é que a regra exista como cenário executável e que a consulta permaneça pequena o suficiente para ser reconhecida como sua tradução.
Quando uma consulta se torna grande demais, há uma pergunta útil: estamos diante de uma regra complexa ou de várias regras misturadas? Uma consulta que reúne autorização, elegibilidade, disponibilidade, faturamento e apresentação provavelmente está fazendo trabalho demais. Separar conceitos não é apenas uma escolha estética. É uma forma de dar a cada decisão um cenário próprio e uma evidência própria.
Em operações mais sensíveis, a mesma disciplina se aplica às transações. Uma regra como “transferir saldo somente quando a conta de origem tiver fundos suficientes e registrar o movimento” não é satisfeita por uma consulta isolada. Ela envolve leitura, decisão e escrita como uma unidade. O Query Builder pode participar dessas operações, mas a especificação precisa expressar o resultado atômico: ou todas as consequências ocorrem, ou nenhuma ocorre.
Um método prático para aplicar amanhã
O caminho mais eficaz não exige reescrever todo o sistema. Escolha uma consulta importante, especialmente uma que envolva permissões, dinheiro, status ou datas, e faça o seguinte.
Primeiro, escreva a regra sem mencionar tabelas ou métodos. Use a perspectiva de quem recebe o resultado. Em vez de “fazer um join com invoices”, escreva “mostrar apenas assinaturas cujo pagamento foi confirmado”.
Segundo, crie pelo menos um exemplo permitido, um exemplo proibido e um exemplo de fronteira. Esses três casos revelam muito mais do que uma longa lista de exemplos semelhantes.
Terceiro, marque cada parte da frase que exige uma decisão. Termos como “ativo”, “recente”, “principal”, “disponível”, “aprovado” e “seu” parecem claros até que se precise transformá-los em predicados.
Quarto, implemente a consulta de modo que sua estrutura possa ser lida como uma tradução da regra. Use nomes qualificados quando houver várias tabelas, selecione apenas os campos necessários e mantenha parâmetros vinculados em vez de concatenar valores diretamente. A clareza aqui também reduz riscos de segurança e de manutenção.
Quinto, revise os cenários contra o banco de dados real. Pergunte se cada teste falharia caso uma condição importante fosse removida. Se a resposta for não, o teste ainda não está medindo a semântica da regra.
Key Takeaways
- Comece pela intenção, não pela tabela: descreva o comportamento que o usuário deve observar antes de escolher
where,joinougroupBy. - Faça cada condição importante justificar sua existência: se a consulta filtra um campo, deve haver uma razão de negócio ou técnica claramente identificável.
- Use contraste nos testes: combine casos permitidos, proibidos e de fronteira para impedir que uma consulta ampla pareça correta.
- Trate datas, status e pertencimento como decisões explícitas: palavras como “recente”, “ativo” e “seu” quase sempre escondem ambiguidades.
- Preserve rastreabilidade: deve ser possível percorrer o caminho da regra ao cenário, do cenário à consulta e da consulta ao resultado observado.
A conexão entre uma linguagem de comportamento e um construtor de consultas aponta para uma mudança mais profunda na engenharia de software. O objetivo não é apenas escrever código que o computador aceite. É construir um sistema no qual as decisões importantes possam ser interrogadas, explicadas e verificadas.
Uma consulta não é somente uma pergunta enviada ao banco. Ela é uma afirmação sobre quem pode ver o quê, em quais condições e por quanto tempo. Quando essa afirmação permanece isolada em uma cadeia de métodos, ela depende da memória de quem a escreveu. Quando é apoiada por cenários claros e dados contrastantes, transforma-se em conhecimento compartilhado.
Talvez a pergunta mais madura durante uma revisão não seja “essa consulta está otimizada?”. Essa pergunta continua importante, mas vem depois de outra: se essa linha estivesse errada, qual comportamento humano seria violado, e qual cenário revelaria a violação?
Quando uma equipe consegue responder, o código deixa de ser apenas uma instrução executável. Ele se torna uma decisão compreensível, testável e defendível. É nesse ponto que dados, regras e linguagem finalmente passam a falar sobre a mesma realidade.
Sources
Hatch New Ideas with Glasp AI 🐣
Glasp AI allows you to hatch new ideas based on your curated content. Let's curate and create with Glasp AI :)
Start Hatching 🐣