O GraphQL, como qualquer tecnologia, tem os seus problemas, alguns deles resultam diretamente da arquitetura e outros são idênticos aos que vemos em qualquer outra aplicação. No entanto, as soluções são completamente diferentes.
Para apresentar o problema, vamos assumir a seguinte arquitetura de aplicação:
E aqui a consulta correspondente em GraphQL para descarregar os dados. Vamos buscar todas as ligações, juntamente com o cartaz e as suas ligações adicionadas ao sistema,
{
allLinks {
id
url
descrição
createdAt
postedBy {
id
name
links {
id
}
}
}
}
Como se vê abaixo, podemos ver aqui o problema clássico de n + 1 com relações.
Carga de links (0.4ms) SELECT "links".* FROM "links" ORDER BY created_at DESC
app/controllers/graphql_controller.rb:5:in `execute'
Carga de utilizadores (0.3ms) SELECT "users".* FROM "users" WHERE "users". "id" = ? LIMIT ? [["id", 40], ["LIMIT", 1]]
↳ app/controllers/graphql_controller.rb:5:in `execute'
Carga de links (0.3ms) SELECT "links".* FROM "links" WHERE "links". "user_id" = ? [["user_id", 40]]
↳ app/controllers/graphql_controller.rb:5:in `execute'
Carga de utilizadores (0.1ms) SELECT "users".* FROM "users" WHERE "users". "id" = ? LIMIT ? [["id", 38], ["LIMIT", 1]]
↳ app/controllers/graphql_controller.rb:5:in `execute'
Carga de links (0.1ms) SELECT "links".* FROM "links" WHERE "links". "user_id" = ? [["user_id", 38]]
↳ app/controllers/graphql_controller.rb:5:in `execute'
Carga de utilizadores (0.2ms) SELECT "users".* FROM "users" WHERE "users". "id" = ? LIMIT ? [["id", 36], ["LIMIT", 1]]
↳ app/controllers/graphql_controller.rb:5:in `execute'
Carga de links (0.1ms) SELECT "links".* FROM "links" WHERE "links". "user_id" = ? [["user_id", 36]]
↳ app/controllers/graphql_controller.rb:5:in `execute'
Carga de utilizadores (0.1ms) SELECT "users".* FROM "users" WHERE "users". "id" = ? LIMIT ? [["id", 34], ["LIMIT", 1]]
↳ app/controllers/graphql_controller.rb:5:in `execute'
Carga de links (0.2ms) SELECT "links".* FROM "links" WHERE "links". "user_id" = ? [["user_id", 34]]
↳ app/controllers/graphql_controller.rb:5:in `execute'
Carga de utilizadores (0.1ms) SELECT "users".* FROM "users" WHERE "users". "id" = ? LIMIT ? [["id", 32], ["LIMIT", 1]]
Neste caso, funciona exatamente como este pedaço de código: Link.all.map(&:utilizador).map(&:ligações).
Parece que conhecemos a solução para o problema: Link.includes(user: :links).map(&:user).map(&:links)mas será que funciona mesmo? Vamos lá ver!
Para verificar a correção, alterei o GraphQL consulta para utilizar apenas alguns campos e nenhuma relação.
{
allLinks {
id
url
descrição
createdAt
}
}
Infelizmente, o resultado mostra que, apesar da falta de ligações em relação ao utilizador e às suas ligações, continuamos a anexar estes dados à consulta da base de dados. Infelizmente, são redundantes e, com uma estrutura ainda mais complicada, revela-se simplesmente ineficaz.
Em GraphQLNo entanto, estes problemas são resolvidos de forma diferente, simplesmente carregando os dados em lotes, assumindo que os dados são necessários quando são colocados na consulta. Trata-se de um carregamento preguiçoso. Uma das bibliotecas mais populares é a https://github.com/Shopify/graphql-batch/.
Infelizmente, a sua instalação não é tão simples como pode parecer. Os carregadores de dados estão disponíveis aqui: https://github.com/Shopify/graphql-batch/tree/master/examples, refiro-me ao Carregador de registos e a classeAssociaçãoLoader classe. Vamos instalar classicamente a classe gem 'graphql-batch' e depois adicioná-la ao nosso esquema, bem como aos carregadores:
# graphql-ruby/app/graphql/graphql_tutorial_schema.rb
classe GraphqlTutorialSchema < GraphQL::Schema
consulta Types::QueryType
mutação Types::MutationType
use GraphQL::Batch
...
end
E os nossos tipos:
# graphql-ruby/app/graphql/types/link_type.rb
módulo Types
classe LinkType < BaseNode
campo :created_at, DateTimeType, null: false
campo :url, String, null: false
campo :description, String, null: false
campo :posted_by, UserType, null: false, método: :user
campo :votos, [Types::VoteType], null: false
def utilizador
Loaders::RecordLoader.for(User).load(object.user_id)
end
end
end
# graphql-ruby/app/graphql/types/user_type.rb
módulo Types
classe UserType < BaseNode
campo :created_at, DateTimeType, null: false
campo :name, String, null: false
campo :email, String, null: false
campo :votos, [VoteType], null: false
campo :links, [LinkType], null: false
def links
Loaders::AssociationLoader.for(User, :links).load(object)
fim
end
fim
Como resultado da utilização dos carregadores, agrupamos os dados e consultamos os dados em duas consultas sql simples:
N + 1 consultas não são tudo, em GraphQL podemos transportar livremente os atributos seguintes. Por defeito, está definido para 1. Por vezes, isto pode ser demasiado para o servidor, especialmente numa situação em que podemos aninhar dados livremente. Como lidar com isso? Podemos limitar a complexidade da consulta, mas, para isso, também precisamos de especificar o seu custo nos atributos. Por defeito, é definido como 1. Definimos este custo utilizando o parâmetro complexidade: onde podemos introduzir dados: campo: links, [LinkType], null: false, complexidade: 101. Para que a limitação funcione efetivamente, é necessário introduzir o limite máximo no seu regime:
classe GraphqlTutorialSchema < GraphQL::Schema
consulta Types::QueryType
mutação Types::MutationType
use GraphQL::Batch
max_complexity 100
...
fim
Rastreio
GraphQL processa as consultas de forma diferente, e o rastreamento não é tão simples se comparado ao que podemos fazer localmente. Infelizmente, o mini-perfilador de rack ou um registo SQL normal não dirá nós tudo e não apontará qual parte da consulta é responsável por uma determinada fatia de tempo. No caso do GraphQL-Ruby, podemos usar soluções comerciais disponíveis aqui: https://graphql-ruby.org/queries/tracingou tentar preparar o nosso próprio rastreio. Abaixo, o snippet parece um rastreador local.
A instalação também é extremamente simples, basta incluir as informações do traçador no esquema traçador (MyCustomTracer.new) configuração. Como no exemplo abaixo:
# graphql-ruby/app/graphql/graphql_tutorial_schema.rb
classe GraphqlTutorialSchema < GraphQL::Schema
consulta Types::QueryType
mutação Types::MutationType
use GraphQL::Batch
tracer(MyCustomTracer.new)
...
fim
O resultado desse rastreio tem o seguinte aspeto:
Iniciou o POST "/graphql" para ::1 em 2021-06-17 22:02:44 +0200
(0.1ms) SELECT sqlite_version(*)
Processamento por GraphqlController#execute como */*
Parâmetros: {"query"=>"{n allLinks {n idn urln descriptionn createdAtn postedBy {n idn namen links {n idn }n }n }n}", "graphql"=>{"query"=>"{n allLinks {n idn urln descriptionn createdAtn postedBy {n idn namen links {n idn }n }n }n}"}}
chave_da_plataforma: graphql.lex, chave: lex, duração: 0.156 ms
chave_da_plataforma: graphql.parse, chave: parse, duração: 0.108 ms
chave_da_plataforma: graphql.validate, chave: validate, duração: 0.537 ms
chave_da_plataforma: graphql.analyze_query, chave: analyze_query, duração: 0.123 ms
platform_key: graphql.analyze_multiplex, key: analyze_multiplex, duration: 0.159 ms
Carga de links (0.4ms) SELECT "links".* FROM "links"
↳ app/graphql/graphql_tutorial_schema.rb:21:in `platform_trace'
platform_key: graphql.execute_query, key: execute_query, duration: 15.562 ms
↳ app/graphql/loaders/record_loader.rb:12:in `perform'
↳ app/graphql/loaders/association_loader.rb:46:in `preload_association'
platform_key: graphql.execute_query_lazy, key: execute_query_lazy, duration: 14.12 ms
platform_key: graphql.execute_multiplex, key: execute_multiplex, duration: 31.11 ms
Concluído 200 OK em 48ms (Visualizações: 1.2ms | ActiveRecord: 2.0ms | Atribuições: 40128)
Resumo
GraphQL já não é uma tecnologia nova, mas as soluções para os seus problemas não estão totalmente normalizadas se não fizerem parte da biblioteca. A implementação desta tecnologia na projeto oferece muitas oportunidades de interação com o frontend e, pessoalmente, considero-o uma nova qualidade em relação ao que a API REST oferece.