O domínio jurídico é, por natureza, um grafo complexo e altamente interconectado. Em um sistema de gestão como o JurisOS, um Caso (processo) possui Contratos associados, que pertencem a Clientes, que por sua vez possuem um histórico de Faturas e pagamentos.
Quando abrimos a plataforma para integrações externas via API, permitir que o cliente navegue por essa árvore de relacionamentos de forma flexível é essencial. O GraphQL brilha nesse cenário, mas traz um perigo clássico: o problema de N+1 queries.
Neste artigo, vamos explorar como o Absinthe (a implementação GraphQL do Elixir) e o Dataloader resolvem esse problema com elegância, mantendo uma separação estrita e limpa entre Schemas, Resolvers e Contextos Ecto.
1. O Fantasma do N+1 em Grafos Jurídicos
Imagine que uma integração externa solicite os últimos 50 casos ativos e, para cada caso, exija os dados do cliente e as faturas pendentes do contrato.
Se resolvermos isso de forma ingênua no GraphQL, o motor executará:
- Uma query para buscar os 50 casos.
- 50 queries individuais para buscar o contrato de cada caso.
- 50 queries para buscar o cliente de cada contrato.
- 50 queries para buscar as faturas.
Total: 151 queries no banco de dados para uma única requisição HTTP. A latência dispara, o pool de conexões do PostgreSQL (ou SQLite no contexto Local-First) sofre exaustão, e a aplicação degrada.
2. A Solução: Batching com Dataloader
O Dataloader atua como uma camada de batching e caching por requisição. Em vez de disparar uma query imediatamente ao resolver o campo cliente de um contrato, ele "anota" a necessidade. Quando todos os contratos daquele nível da árvore forem resolvidos, o Dataloader consolida os IDs e dispara uma única query utilizando a cláusula IN:
-- O que era 50 queries vira apenas 1:
SELECT * FROM clientes WHERE id IN (1, 2, 3, ..., 50);
No ecossistema Elixir, a biblioteca dataloader se integra perfeitamente ao Ecto e ao Absinthe.
3. Arquitetura Limpa: Contextos, Resolvers e Schemas
Para manter o código sustentável, o JurisOS adota fronteiras claras. O GraphQL não deve vazar para a camada de negócios, e os Contextos não devem saber que estão servindo a uma API.
Passo 1: O Contexto Ecto (A Fonte da Verdade)
O Contexto expõe a API pública do domínio, utilizando Ecto puro. Para integrar com o Dataloader, implementamos a função datasource/0, que expõe a estrutura de dados para o GraphQL fazer o batching.
defmodule JurisOS.Clientes do
alias JurisOS.Repo
alias JurisOS.Clientes.Cliente
# Lógica de negócio padrão
def list_clientes, do: Repo.all(Cliente)
def get_cliente!(id), do: Repo.get!(Cliente, id)
# Integração com o Dataloader
def datasource() do
Dataloader.Ecto.new(Repo, query: &query/2)
end
# Permite customizar as queries do dataloader (ex: soft deletes)
def query(queryable, _params) do
queryable
end
end
Passo 2: O Resolver (A Cola)
O Resolver conecta os queries e mutations do GraphQL aos Contextos. Com o Dataloader, muitas vezes não precisamos escrever resolvers manuais para relacionamentos simples, mas para buscas raízes (root queries), o resolver continua sendo o maestro.
defmodule JurisOSWeb.Resolvers.JuridicoResolver do
alias JurisOS.Juridico
# Resolve a query raiz de casos
def listar_casos_ativos(_parent, _args, _resolution) do
{:ok, Juridico.list_casos_ativos()}
end
end
Passo 3: O Schema GraphQL (O Contrato)
No Schema, declaramos os tipos e instruímos o Absinthe a usar o Dataloader para os relacionamentos.
defmodule JurisOSWeb.Schema.Types.JuridicoTypes do
use Absinthe.Schema.Notation
import Absinthe.Resolution.Helpers, only: [dataloader: 1]
object :caso do
field :id, :id
field :titulo, :string
field :numero_processo, :string
# O Dataloader resolve a relação de N+1 automaticamente!
field :contrato, :contrato, resolve: dataloader(JurisOS.Contratos)
end
object :contrato do
field :id, :id
field :valor, :decimal
# Busca o cliente e as faturas em lote
field :cliente, :cliente, resolve: dataloader(JurisOS.Clientes)
field :faturas, list_of(:fatura), resolve: dataloader(JurisOS.Financeiro)
end
end
Passo 4: Conectando o Dataloader no Schema Principal
Por fim, inicializamos o contexto do Dataloader no context/1 do Absinthe e registramos os nossos domínios.
defmodule JurisOSWeb.Schema do
use Absinthe.Schema
import_types JurisOSWeb.Schema.Types.JuridicoTypes
# import de outros tipos...
query do
@desc "Obtém a lista de casos ativos"
field :casos, list_of(:caso) do
resolve &JurisOSWeb.Resolvers.JuridicoResolver.listar_casos_ativos/3
end
end
# Setup do Dataloader
def context(ctx) do
loader =
Dataloader.new()
|> Dataloader.add_source(JurisOS.Contratos, JurisOS.Contratos.datasource())
|> Dataloader.add_source(JurisOS.Clientes, JurisOS.Clientes.datasource())
|> Dataloader.add_source(JurisOS.Financeiro, JurisOS.Financeiro.datasource())
Map.put(ctx, :loader, loader)
end
def plugins do
[Absinthe.Middleware.Dataloader] ++ Absinthe.Plugin.defaults()
end
end
Conclusão
Com o Absinthe e o Dataloader, a API externa do JurisOS permite que integrações de terceiros naveguem por estruturas jurídicas complexas de forma declarativa e extremamente performática.
Reduzimos 151 queries para apenas 4, sem poluir a camada de negócios com lógica de otimização de banco de dados. O Contexto Ecto continua puro, os Resolvers permanecem magros, e o Schema atua de forma expressiva documentando o contrato da API. Essa é a verdadeira beleza do ecossistema Elixir para APIs GraphQL de alto nível!
Top comments (0)