GraphQL, como cualquier tecnología, tiene sus problemas, algunos de ellos resultan directamente de la arquitectura y otros son idénticos a los que vemos en cualquier otra aplicación. Sin embargo, las soluciones son completamente diferentes.
Para presentar el problema, supongamos la siguiente arquitectura de aplicación:
Y aquí la consulta correspondiente en GraphQL para descargar los datos. Recuperamos todos los enlaces, junto con el cartel y sus enlaces añadidos al sistema,
{
allLinks {
id
url
descripción
createdAt
postedBy {
id
nombre
enlaces {
id
}
}
}
}
Como se muestra a continuación, aquí podemos ver el clásico problema n + 1 con relaciones.
Carga de enlaces (0.4ms) SELECT "enlaces".* FROM "enlaces" ORDER BY created_at DESC
↳ app/controllers/graphql_controller.rb:5:in `execute'
Carga de usuarios (0.3ms) SELECT "users".* FROM "users" WHERE "users". "id" = ? LIMIT ? [["id", 40], ["LIMIT", 1]]
↳ app/controllers/graphql_controller.rb:5:in `execute'
Carga de enlaces (0.3ms) SELECT "enlaces".* FROM "enlaces" WHERE "enlaces". "user_id" = ? [["user_id", 40]]
↳ app/controllers/graphql_controller.rb:5:in `execute'
Carga de usuarios (0.1ms) SELECT "users".* FROM "users" WHERE "users". "id" = ? LIMIT ? [["id", 38], ["LIMIT", 1]]
↳ app/controllers/graphql_controller.rb:5:in `execute'
Carga de enlaces (0.1ms) SELECT "enlaces".* FROM "enlaces" WHERE "enlaces". "user_id" = ? [["user_id", 38]]
↳ app/controllers/graphql_controller.rb:5:in `execute'
Carga de usuarios (0.2ms) SELECT "users".* FROM "users" WHERE "users". "id" = ? LIMIT ? [["id", 36], ["LIMIT", 1]]
↳ app/controllers/graphql_controller.rb:5:in `execute'
Carga de enlaces (0.1ms) SELECT "enlaces".* FROM "enlaces" WHERE "enlaces". "user_id" = ? [["user_id", 36]]
↳ app/controllers/graphql_controller.rb:5:in `execute'
Carga de usuarios (0.1ms) SELECT "users".* FROM "users" WHERE "users". "id" = ? LIMIT ? [["id", 34], ["LIMIT", 1]]
↳ app/controllers/graphql_controller.rb:5:in `execute'
Carga de enlaces (0.2ms) SELECT "enlaces".* FROM "enlaces" WHERE "enlaces". "user_id" = ? [["user_id", 34]]
↳ app/controllers/graphql_controller.rb:5:in `execute'
Carga de usuarios (0.1ms) SELECT "users".* FROM "users" WHERE "users". "id" = ? LIMIT ? [["id", 32], ["LIMIT", 1]]
En este caso, funciona exactamente igual que esta pieza de código: Link.all.map(&:usuario).map(&:enlaces).
Parece que conocemos la solución al problema: Link.includes(usuario: :enlaces).map(&:usuario).map(&:enlaces)pero, ¿funcionará realmente? ¡Vamos a comprobarlo!
Para verificar el arreglo, cambié el GraphQL consulta para utilizar sólo unos pocos campos y ninguna relación.
{
allLinks {
id
url
descripción
createdAt
}
}
Por desgracia, el resultado muestra que, a pesar de la falta de vínculos en relación con el usuario y sus enlaces, seguimos adjuntando estos datos a la consulta de la base de datos. Por desgracia, son redundantes y, con una estructura aún más complicada, resulta ser simplemente ineficaz.
En GraphQLSin embargo, este tipo de problemas se resuelven de otra manera, simplemente cargando los datos por lotes, suponiendo que los datos son necesarios cuando se introducen en la consulta. Se trata de una carga perezosa. Una de las bibliotecas más populares es https://github.com/Shopify/graphql-batch/.
Por desgracia, su instalación no es tan sencilla como parece. Los cargadores de datos están disponibles aquí: https://github.com/Shopify/graphql-batch/tree/master/examples, me refiero al Cargador de grabaciones y la claseCargador de asociaciones clase. Instalemos clásicamente la clase gem 'graphql-batch' y luego añadirlo a nuestro esquema, así como cargadores:
# graphql-ruby/app/graphql/graphql_tutorial_schema.rb
class GraphqlTutorialSchema < GraphQL::Esquema
consulta Types::QueryType
mutación Types::MutationType
use GraphQL::Lote
...
end
Y nuestros tipos:
# graphql-ruby/app/graphql/types/link_type.rb
módulo Types
clase LinkType < BaseNode
campo :created_at, DateTimeType, null: false
campo :url, String, null: false
field :description, String, null: false
field :posted_by, UserType, null: false, method: :usuario
field :votes, [Types::VoteType], null: false
def usuario
Loaders::RecordLoader.for(User).load(object.user_id)
end
end
end
# graphql-ruby/app/graphql/types/user_type.rb
módulo Types
clase TipoUsuario < NodoBase
campo :created_at, DateTimeType, null: false
campo :name, String, null: false
field :email, String, null: false
field :votes, [VoteType], null: false
field :links, [LinkType], null: false
def enlaces
Loaders::AssociationLoader.for(Usuario, :enlaces).load(objeto)
end
end
end
Como resultado del uso de los cargadores, procesamos los datos por lotes y los consultamos en dos simples consultas sql:
También hay otras soluciones que resuelven este problema, como:
Complejidad de las consultas
N + 1 consultas no lo son todo, en GraphQL podemos traspasar libremente los siguientes atributos. Por defecto, se establece en 1. Esto a veces puede ser demasiado para el servidor, especialmente en una situación en la que podemos anidar datos libremente. ¿Cómo solucionarlo? Podemos limitar la complejidad de la consulta, pero para ello, también tenemos que especificar su coste en los atributos. Por defecto se establece en 1. Fijamos este coste utilizando el atributo complejidad: donde podemos introducir los datos: campo: links, [LinkType], null: false, complejidad: 101. Para que la limitación funcione de verdad, hay que introducir el límite máximo en el régimen:
class GraphqlTutorialSchema < GraphQL::Schema
consulta Types::QueryType
mutación Types::MutationType
use GraphQL::Lote
max_complexity 100
...
end
Rastreando
GraphQL procesa las consultas de forma diferente, y el rastreo no es tan sencillo si lo comparamos con lo que podemos hacer localmente. Desgraciadamente, el mini perfilador de rack o un log SQL normal no nos lo dirán todo y no señalarán qué parte de la consulta es responsable de un tramo de tiempo determinado. En el caso de GraphQL-Ruby, podemos utilizar soluciones comerciales disponibles aquí: https://graphql-ruby.org/queries/tracingo intentar preparar nuestro propio rastreo. A continuación, el fragmento se parece a un trazador local.
La instalación también es extremadamente sencilla, basta con incluir la información del trazador en el esquema tracer (MyCustomTracer.new) configuración. Como en el ejemplo siguiente:
# graphql-ruby/app/graphql/graphql_tutorial_schema.rb
class GraphqlTutorialSchema < GraphQL::Esquema
consulta Types::QueryType
mutación Types::MutationType
use GraphQL::Lote
tracer(MyCustomTracer.new)
...
end
La salida de dicho rastreo tiene el siguiente aspecto:
Iniciado POST "/graphql" para ::1 en 2021-06-17 22:02:44 +0200
(0.1ms) SELECT sqlite_version(*)
Procesado por GraphqlController#execute como */*
Parámetros: {"query"=>"{n allLinks {n idn urln descriptionn createdAtn postedBy {n idn namen links {n idn }n }n }n }n}", "graphql"=>{"query"=>"{n allLinks {n idn urln descriptionn createdAtn postedBy {n idn namen links {n idn }n }n }n }n }n}"}}}
clave_plataforma: graphql.lex, clave: lex, duración: 0.156 ms
platform_key: graphql.parse, clave: parse, duración: 0.108 ms
platform_key: graphql.validate, clave: validate, duración: 0,537 ms 0.537 ms
platform_key: graphql.analyze_query, clave: analyze_query, duración: 0,123 ms 0.123 ms
platform_key: graphql.analyze_multiplex, clave: analyze_multiplex, duración: 0,159 ms 0.159 ms
Carga de enlaces (0,4 ms) SELECT "enlaces".* FROM "enlaces"
↳ app/graphql/graphql_tutorial_schema.rb:21:in `platform_trace'
platform_key: graphql.execute_query, clave: execute_query, duración: 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, clave: execute_multiplex, duración: 31.11 ms
Completado 200 OK en 48ms (Vistas: 1.2ms | ActiveRecord: 2.0ms | Asignaciones: 40128)
Resumen
GraphQL ya no es una tecnología nueva, pero las soluciones a sus problemas no están totalmente normalizadas si no forman parte de la biblioteca. La implantación de esta tecnología en la proyecto ofrece muchas oportunidades para interactuar con el frontend y personalmente lo considero una nueva cualidad en relación con lo que ofrece REST API.