GraphQL, comme toute technologie, a ses problèmes, dont certains résultent directement de l'architecture et d'autres sont identiques à ce que nous voyons dans n'importe quelle autre application. Cependant, les solutions sont complètement différentes.
Pour présenter le problème, supposons l'architecture d'application suivante :
Et voici la requête correspondante en GraphQL pour télécharger les données. Nous récupérons tous les liens, ainsi que l'affiche et ses liens ajoutés au système,
{
allLinks {
id
url
description
createdAt
postedBy {
id
nom
links {
id
}
}
}
}
Comme le montre l'illustration ci-dessous, on retrouve ici le problème classique des relations n + 1.
Chargement des liens (0.4ms) SELECT "links".* FROM "links" ORDER BY created_at DESC
↳ app/controllers/graphql_controller.rb:5:in `execute'
Chargement des utilisateurs (0.3ms) SELECT "users".* FROM "users" WHERE "users". "id" = ? LIMIT ? [["id", 40], ["LIMIT", 1]]
↳ app/controllers/graphql_controller.rb:5:in `execute'
Chargement des liens (0.3ms) SELECT "links".* FROM "links" WHERE "links". "user_id" = ? [["user_id", 40]]
↳ app/controllers/graphql_controller.rb:5:in `execute'
Chargement des utilisateurs (0.1ms) SELECT "users".* FROM "users" WHERE "users". "id" = ? LIMIT ? [["id", 38], ["LIMIT", 1]]
↳ app/controllers/graphql_controller.rb:5:in `execute'
Chargement des liens (0.1ms) SELECT "links".* FROM "links" WHERE "links". "user_id" = ? [["user_id", 38]]
↳ app/controllers/graphql_controller.rb:5:in `execute'
Chargement des utilisateurs (0.2ms) SELECT "users".* FROM "users" WHERE "users". "id" = ? LIMIT ? [["id", 36], ["LIMIT", 1]]
↳ app/controllers/graphql_controller.rb:5:in `execute'
Chargement des liens (0.1ms) SELECT "links".* FROM "links" WHERE "links". "user_id" = ? [["user_id", 36]]
↳ app/controllers/graphql_controller.rb:5:in `execute'
Chargement des utilisateurs (0.1ms) SELECT "users".* FROM "users" WHERE "users". "id" = ? LIMIT ? [["id", 34], ["LIMIT", 1]]
↳ app/controllers/graphql_controller.rb:5:in `execute'
Chargement des liens (0.2ms) SELECT "links".* FROM "links" WHERE "links". "user_id" = ? [["user_id", 34]]
↳ app/controllers/graphql_controller.rb:5:in `execute'
Chargement des utilisateurs (0.1ms) SELECT "users".* FROM "users" WHERE "users". "id" = ? LIMIT ? [["id", 32], ["LIMIT", 1]]
Dans ce cas, cela fonctionne exactement comme ce morceau de code: Link.all.map(&:user).map(&:links).
Nous semblons connaître la solution au problème : Link.includes(user : :links).map(&:user).map(&:links)mais est-ce que ça marche vraiment ? Vérifions-le !
Pour vérifier la correction, j'ai modifié le fichier GraphQL pour n'utiliser que quelques champs et aucune relation.
{
allLinks {
id
url
description
createdAt
}
}
Malheureusement, le résultat montre que, malgré l'absence de liens en rapport avec l'utilisateur et ses liens, nous attachons toujours ces données à la requête de base de données. Malheureusement, elles sont redondantes et, avec une structure encore plus compliquée, cela s'avère tout simplement inefficace.
En GraphQLEn revanche, ces problèmes sont résolus différemment, simplement en chargeant les données par lots, en supposant que les données sont nécessaires au moment où elles sont introduites dans la requête. Il s'agit d'un chargement paresseux. L'une des bibliothèques les plus populaires est https://github.com/Shopify/graphql-batch/.
Malheureusement, son installation n'est pas aussi simple qu'il n'y paraît. Les chargeurs de données sont disponibles ici : https://github.com/Shopify/graphql-batch/tree/master/examples, je veux dire le fichier RecordLoader et la classeAssociationLoader classe. Installons classiquement la classe gem 'graphql-batch' et l'ajouter à notre schéma, ainsi que les chargeurs :
# graphql-ruby/app/graphql/graphql_tutorial_schema.rb
class GraphqlTutorialSchema < GraphQL::Schema
query Types::QueryType
mutation Types::MutationType
use GraphQL::Batch
...
fin
Et nos types :
# graphql-ruby/app/graphql/types/link_type.rb
module Types
classe LinkType < BaseNode
field :created_at, DateTimeType, null : false
field :url, String, null : false
field :description, String, null : false
champ :posted_by, UserType, null : false, method : :user
champ :votes, [Types::VoteType], null : false
def user
Loaders::RecordLoader.for(User).load(object.user_id)
end
end
end
# graphql-ruby/app/graphql/types/user_type.rb
module Types
classe UserType < BaseNode
champ :created_at, DateTimeType, null : false
champ :name, Chaîne, null : false
champ :email, Chaîne, null : false
champ :votes, [VoteType], null : false
champ :links, [LinkType], null : false
def links
Loaders::AssociationLoader.for(User, :links).load(object)
end
end
end
Grâce à l'utilisation des chargeurs, nous mettons les données en lots et nous les interrogeons au moyen de deux requêtes SQL simples :
Démarrage du POST "/graphql" pour ::1 à 2021-06-16 22:40:17 +0200
(0.1ms) SELECT sqlite_version(*)
Traitement par GraphqlController#execute en tant que */*
Paramètres : {"query"=>"{n allLinks {n idn urln descriptionn createdAtn postedBy {n idn namen links {n idn }n }n}", "graphql"=>{"query"=>"{n allLinks {n idn urln descriptionn createdAtn postedBy {n idn namen links {n idn }n }n}"}
Chargement des liens (0.4ms) SELECT "links".* FROM "links"
↳ app/controllers/graphql_controller.rb:5:in `execute'
Chargement des utilisateurs (0.9ms) SELECT "users".* FROM "users" WHERE "users". "id" IN ( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) [["id", 2], ["id", 4], ["id", 6], ["id", 8], ["id", 10], ["id", 12], ["id", 14], ["id", 16], ["id", 18], ["id", 20], ["id", 22], ["id", 24], ["id", 26], ["id", 28], ["id", 30], ["id", 32], ["id", 34], ["id", 36], ["id", 38], ["id", 40].
↳ app/graphql/loaders/record_loader.rb:12:in `perform'
Chargement des liens (0.5ms) SELECT "links".* FROM "links" WHERE "links". "user_id" IN ( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) [["user_id", 2], ["user_id", 4], ["user_id", 6], ["user_id", 8], ["user_id", 10], ["user_id", 12], ["user_id", 14], ["user_id", 16], ["user_id", 18], ["user_id", 20], ["user_id", 22], ["user_id", 24], ["user_id", 26], ["user_id", 28], ["user_id", 30], ["user_id", 32], ["user_id", 34], ["user_id", 36], ["user_id", 38], ["user_id", 40].
↳ app/graphql/loaders/association_loader.rb:46:in `preload_association'
Terminé 200 OK en 62ms (Vues : 1.3ms | ActiveRecord : 1.8ms | Allocations : 39887)
Il existe également d'autres solutions pour résoudre ce problème, comme par exemple :
Complexité des requêtes
N + 1 requêtes ne sont pas tout, en GraphQL nous pouvons librement reporter les attributs suivants. Par défaut, il est fixé à 1. Cela peut parfois être trop pour le serveur, en particulier dans une situation où nous pouvons librement imbriquer des données. Comment y remédier ? Nous pouvons limiter la complexité de la requête, mais pour ce faire, nous devons également spécifier leur coût dans les attributs. Par défaut, il est fixé à 1. Nous définissons ce coût à l'aide de la fonction complexité : où l'on peut saisir des données : champ : links, [LinkType], null : false, complexity : 101. Pour que la limitation fonctionne réellement, vous devez encore introduire la limite maximale dans votre régime :
class GraphqlTutorialSchema < GraphQL::Schema
query Types::QueryType
mutation Types::MutationType
use GraphQL::Batch
max_complexity 100
...
fin
Traçage
GraphQL traite les requêtes différemment, et le traçage n'est pas si simple si on le compare à ce que l'on peut faire localement. Malheureusement, le rack mini profiler ou un log SQL classique ne nous dira pas tout et ne nous indiquera pas quelle partie de la requête est responsable d'une tranche de temps donnée. Dans le cas de GraphQL-Ruby, nous pouvons utiliser des solutions commerciales disponibles ici : https://graphql-ruby.org/queries/tracingou essayer de préparer notre propre traçage. Ci-dessous, l'extrait ressemble à un traceur local.
# lib/my_custom_tracer.rb
class MyCustomTracer 'graphql.lex',
'parse' => 'graphql.parse',
'validate' => 'graphql.validate',
'analyze_query' => 'graphql.analyze_query',
'analyze_multiplex' => 'graphql.analyze_multiplex',
'execute_multiplex' => 'graphql.execute_multiplex',
'execute_query' => 'graphql.execute_query',
'execute_query_lazy' => 'graphql.execute_query_lazy'
}
def platform_trace(platform_key, key, _data, &block)
start = ::Process.clock_gettime ::Process::CLOCK_MONOTONIC
result = block.call
duration = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) - start
observe(clé_plateforme, clé, durée)
résultat
fin
def platform_field_key(type, field)
"graphql.#{type.nom.graphql_}.#{champ.nom.graphql_}"
end
def platform_authorized_key(type)
"graphql.authorized.#{type.graphql_name}"
end
def plateforme_resolve_type_key(type)
"graphql.resolve_type.#{type.graphql_name}"
end
def observe(clé_de_la_plateforme, clé, durée)
return if key == 'authorized'
puts "clé_plateforme : #{clé_plateforme}, clé : #{clé}, durée : #{(duration * 1000).round(5)} ms".yellow
fin
fin
L'installation est également extrêmement simple, il suffit d'inclure les informations relatives au traceur dans le schéma tracer (MyCustomTracer.new) configuration. Comme dans l'exemple ci-dessous :
# graphql-ruby/app/graphql/graphql_tutorial_schema.rb
class GraphqlTutorialSchema < GraphQL::Schema
query Types::QueryType
mutation Types::MutationType
use GraphQL::Batch
tracer(MyCustomTracer.new)
...
fin
Le résultat d'un tel traçage ressemble à ceci :
Démarrage du POST "/graphql" pour ::1 à 2021-06-17 22:02:44 +0200
(0.1ms) SELECT sqlite_version(*)
Traitement par GraphqlController#execute en tant que */*
Paramètres : {"query"=>"{n allLinks {n idn urln descriptionn createdAtn postedBy {n idn namen links {n idn }n }n}", "graphql"=>{"query"=>"{n allLinks {n idn urln descriptionn createdAtn postedBy {n idn namen links {n idn }n }n}"}}
platform_key : graphql.lex, key : lex, duration : 0.156 ms
platform_key : graphql.parse, key : parse, duration : 0.108 ms
platform_key : graphql.validate, key : validate, duration : 0.537 ms
platform_key : graphql.analyze_query, key : analyze_query, duration : 0,123 ms
platform_key : graphql.analyze_multiplex, key : analyze_multiplex, duration : 0.159 ms
Chargement des liens (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
Terminé 200 OK en 48ms (Vues : 1.2ms | ActiveRecord : 2.0ms | Allocations : 40128)
Résumé
GraphQL n'est plus une technologie nouvelle, mais les solutions à ses problèmes ne sont pas entièrement normalisées si elles ne font pas partie de la bibliothèque. La mise en œuvre de cette technologie dans la projet offre de nombreuses possibilités d'interaction avec le frontend et je considère personnellement qu'il s'agit d'une nouvelle qualité par rapport à ce qu'offre l'API REST.