DEV Community

GraphQL Elegante com Absinthe & Dataloader: Estruturando Consultas de Grafos Complexos no Direito

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á:

  1. Uma query para buscar os 50 casos.
  2. 50 queries individuais para buscar o contrato de cada caso.
  3. 50 queries para buscar o cliente de cada contrato.
  4. 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);
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)