GraphQL heeft, net als elke technologie, zijn problemen, waarvan sommige direct voortvloeien uit de architectuur en sommige identiek zijn aan wat we in elke andere toepassing zien. De oplossingen zijn echter totaal verschillend.
Om het probleem voor te stellen, gaan we uit van de volgende applicatiearchitectuur:
En hier de overeenkomstige query in GraphQL om de gegevens te downloaden. We halen alle links op, samen met de poster en de links die aan het systeem zijn toegevoegd,
{
alleLinks {
id
url
beschrijving
createdAt
postedBy {
id
naam
links {
id
}
}
}
}
Zoals hieronder weergegeven, zien we hier het klassieke n + 1 probleem met relaties.
Link laden (0.4ms) SELECT "links".* FROM "links" ORDER BY created_at DESC
app/controllers/graphql_controller.rb:5:in `uitvoeren
Gebruiker laden (0.3ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 40], ["LIMIT", 1]]
↳ app/controllers/graphql_controller.rb:5:in `uitvoeren
Link laden (0.3ms) SELECT "links".* FROM "links" WHERE "links"."user_id" = ? [["user_id", 40]]
↳ app/controllers/graphql_controller.rb:5:in `uitvoeren
Gebruiker laden (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 38], ["LIMIT", 1]]
↳ app/controllers/graphql_controller.rb:5:in `uitvoeren
Link laden (0.1ms) SELECT "links".* FROM "links" WHERE "links"."user_id" = ? [["user_id", 38]]
↳ app/controllers/graphql_controller.rb:5:in `uitvoeren
Gebruiker laden (0.2ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 36], ["LIMIT", 1]]
↳ app/controllers/graphql_controller.rb:5:in `uitvoeren
Link laden (0.1ms) SELECT "links".* FROM "links" WHERE "links"."user_id" = ? [["user_id", 36]]
↳ app/controllers/graphql_controller.rb:5:in `uitvoeren
Gebruiker laden (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 34], ["LIMIT", 1]]
↳ app/controllers/graphql_controller.rb:5:in `uitvoeren
Link laden (0.2ms) SELECT "links".* FROM "links" WHERE "links"."user_id" = ? [["user_id", 34]]
↳ app/controllers/graphql_controller.rb:5:in `uitvoeren
Gebruiker laden (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 32], ["LIMIT", 1]]
In dit geval werkt het precies zoals dit stukje code: Link.all.map(&:user).map(&:links).
We lijken de oplossing voor het probleem te kennen: Link.includes(user: :links).map(&:user).map(&:links)Maar zal het echt werken? Laten we het uitproberen!
Om de fix te verifiëren, veranderde ik de GraphQL query om slechts een paar velden te gebruiken en geen relatie.
{
alleLinks {
id
url
beschrijving
createdAt
}
}
Helaas laat het resultaat zien dat, ondanks het gebrek aan links met betrekking tot de gebruiker en hun links, we deze gegevens nog steeds koppelen aan een database query. Helaas zijn ze overbodig en, met een nog ingewikkelder structuur, blijkt het gewoon inefficiënt te zijn.
In GraphQLDergelijke problemen worden op een andere manier opgelost, namelijk door gegevens in batches te laden, ervan uitgaande dat de gegevens nodig zijn op het moment dat ze in de query worden gezet. Het is zo'n luie manier van laden. Een van de populairste bibliotheken is https://github.com/Shopify/graphql-batch/.
Helaas is de installatie ervan niet zo probleemloos als het lijkt. De dataladers zijn hier beschikbaar: https://github.com/Shopify/graphql-batch/tree/master/examples, ik bedoel de RecordLoader klasse en deVerenigingslader klasse. Laten we klassiek de gem 'graphql-batch' bibliotheek en voeg deze dan toe aan ons schema, evenals loaders:
# graphql-ruby/app/graphql/graphql_tutorial_schema.rb
klasse GraphqlTutorialSchema < GraphQL::Schema
query Types::QueryType
mutatie Types::MutationType
gebruik GraphQL::Batch
...
einde
Er zijn ook andere oplossingen die dit probleem oplossen, zoals:
Complexiteit van query's
N + 1 query's zijn niet alles, in GraphQL kunnen we de volgende attributen vrij overdragen. Standaard staat dit op 1. Dit kan soms te veel zijn voor de server, vooral in een situatie waarin we vrij gegevens kunnen nesten. Hoe gaan we hiermee om? We kunnen de complexiteit van de query beperken, maar daarvoor moeten we ook de kosten ervan in de attributen opgeven. Standaard zijn deze ingesteld op 1. We stellen deze kosten in met de complexiteit: attribuut, waar we gegevens kunnen invoeren: veld: links, [LinkType], nul: onwaar, complexiteit: 101. Als beperking echt moet werken, moet je nog steeds de maximumlimiet invoeren in je regeling:
klasse GraphqlTutorialSchema < GraphQL::Schema
query Types::QueryType
mutatie Types::MutationType
gebruik GraphQL::Batch
max_complexiteit 100
...
einde
Opsporen
GraphQL queries anders verwerkt en traceren niet zo eenvoudig is als je het vergelijkt met wat we lokaal kunnen doen. Helaas zal de rack mini profiler of een gewone SQL log ons niet alles vertellen en zal niet aangeven welk deel van de query verantwoordelijk is voor een bepaalde tijdspanne. In het geval van GraphQL-Ruby kunnen we commerciële oplossingen gebruiken die hier beschikbaar zijn: https://graphql-ruby.org/queries/tracingof proberen onze eigen tracing voor te bereiden. Hieronder ziet het fragment eruit als een lokale tracer.
Installatie is ook heel eenvoudig, je moet de tracerinformatie opnemen in het schema tracer (MyCustomTracer.new) configuratie. Zoals in het onderstaande voorbeeld:
# graphql-ruby/app/graphql/graphql_tutorial_schema.rb
klasse GraphqlTutorialSchema < GraphQL::Schema
query Types::QueryType
mutatie Types::MutationType
gebruik GraphQL::Batch
tracer(MyCustomTracer.new)
...
einde
De uitvoer van een dergelijke tracing ziet er als volgt uit:
Gestart POST "/graphql" voor ::1 op 2021-06-17 22:02:44 +0200
(0.1ms) SELECT sqlite_version(*)
Verwerking door GraphqlController#execute als */*
Parameters: {"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, sleutel: lex, duur: 0.156 ms
platform_key: graphql.parse, sleutel: parse, duur: 0,108 ms
platform_key: graphql.validate, sleutel: validate, duur: 0,537 ms 0,537 ms
platform_key: graphql.analyze_query, sleutel: analyze_query, duur: 0,123 ms
platform_key: graphql.analyze_multiplex, sleutel: analyze_multiplex, duur: 0,159 ms
Linkbelasting (0,4 ms) SELECT "links".* FROM "links".
↳ app/graphql/graphql_tutorial_schema.rb:21:in `platform_trace'
platform_key: graphql.execute_query, sleutel: execute_query, duur: 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, sleutel: execute_query_lazy, duur: 14.12 ms
platform_key: graphql.execute_multiplex, key: execute_multiplex, duration: 31.11 ms
Voltooid 200 OK in 48 ms (Views: 1.2 ms | ActiveRecord: 2.0 ms | Toewijzingen: 40128)
Samenvatting
GraphQL is geen nieuwe technologie meer, maar de oplossingen voor de problemen zijn niet volledig gestandaardiseerd als ze geen deel uitmaken van de bibliotheek. De implementatie van deze technologie in de project geeft veel mogelijkheden voor interactie met de frontend en ik beschouw het persoonlijk als een nieuwe kwaliteit ten opzichte van wat REST API biedt.