GraphQL har som all teknik sina problem, vissa av dem är en direkt följd av arkitekturen och andra är identiska med vad vi ser i alla andra applikationer. Lösningarna är dock helt olika.
För att presentera problemet antar vi följande applikationsarkitektur:
Och här är motsvarande fråga i GraphQL för att ladda ner data. Vi hämtar alla länkar, tillsammans med affischen och dess länkar som lagts till i systemet,
{
allaLänkar {
id
url
beskrivning
skapadAt
postedBy {
id
namn
länkar {
id
}
}
}
}
Som visas nedan kan vi se det klassiska n + 1-problemet med relationer här.
Laddning av länkar (0,4 ms) SELECT "links".* FROM "links" ORDER BY created_at DESC
↳ app/controllers/graphql_controller.rb:5:in `execute'
Användarbelastning (0,3 ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 40], ["LIMIT", 1]] [["id", 40], ["LIMIT", 1]]
↳ app/controllers/graphql_controller.rb:5:in `execute'
Laddning av länkar (0,3 ms) SELECT "links".* FROM "links" WHERE "links"."user_id" = ? [["user_id", 40]]
↳ app/controllers/graphql_controller.rb:5:in `execute'
Användarbelastning (0,1 ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 38], ["LIMIT", 1]] [["id", 38], ["LIMIT", 1]]
↳ app/controllers/graphql_controller.rb:5:in `execute'
Laddning av länkar (0,1 ms) SELECT "links".* FROM "links" WHERE "links"."user_id" = ? [["user_id", 38]]
↳ app/controllers/graphql_controller.rb:5:in `execute'
Användarbelastning (0,2 ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 36], ["LIMIT", 1]] [["id", 36], ["LIMIT", 1]]
↳ app/controllers/graphql_controller.rb:5:in `execute'
Laddning av länkar (0,1 ms) SELECT "links".* FROM "links" WHERE "links"."user_id" = ? [["user_id", 36]]
↳ app/controllers/graphql_controller.rb:5:in `execute'
Användarbelastning (0,1 ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 34], ["LIMIT", 1]] [["id", 34], ["LIMIT", 1]]
↳ app/controllers/graphql_controller.rb:5:in `execute'
Laddning av länkar (0,2 ms) SELECT "links".* FROM "links" WHERE "links"."user_id" = ? [["user_id", 34]]
↳ app/controllers/graphql_controller.rb:5:in `execute'
Användarbelastning (0,1 ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 32], ["LIMIT", 1]] [["id", 32], ["LIMIT", 1]]
I det här fallet fungerar det precis som den här biten av kod: Länk.alla.karta(&:användare).karta(&:länkar).
Vi verkar veta lösningen på problemet: Link.includes(användare: :länkar).map(&:användare).map(&:länkar)men kommer det verkligen att fungera? Låt oss kolla upp det!
För att verifiera lösningen ändrade jag GraphQL fråga för att bara använda några få fält och ingen relation.
{
allaLänkar {
id
url
beskrivning
skapadAt
}
}
Tyvärr visar resultatet att vi, trots avsaknaden av länkar i förhållande till användaren och deras länkar, fortfarande bifogar dessa uppgifter till databasfrågan. Tyvärr är de överflödiga och med en ännu mer komplicerad struktur visar det sig helt enkelt vara ineffektivt.
I GraphQLSådana problem löses på ett annat sätt, helt enkelt genom att ladda data i omgångar och anta att datan behövs när den läggs in i frågan. Det är en sådan lat laddning. Ett av de mest populära biblioteken är https://github.com/Shopify/graphql-batch/.
Tyvärr är installationen inte så problemfri som det kan verka. Dataladdarna finns tillgängliga här: https://github.com/Shopify/graphql-batch/tree/master/examples, jag menar RecordLoader klassen ochAssociationLoader klass. Låt oss på klassiskt sätt installera gem 'graphql-batch' bibliotek och sedan lägga till det i vårt schema, samt laddare:
# graphql-ruby/app/graphql/graphql_tutorial_schema.rb
class GraphqlTutorialSchema < GraphQL::Schema
query Typer::QueryType
mutation Typer::MutationType
använd GraphQL::Batch
...
slut
Och våra typer:
# graphql-ruby/app/graphql/types/link_type.rb
modul Typer
klass LinkType < Basnod
fält :created_at, DateTimeType, null: false
fält :url, Sträng, null: false
field :description, Sträng, null: false
field :posted_by, UserType, null: false, method: :user
field :votes, [Types::VoteType], null: false
def användare
Loaders::RecordLoader.for(User).load(objekt.user_id)
slut
slut
slut
# graphql-ruby/app/graphql/types/user_type.rb
modul Typer
klass UserType < Basnod
fält :created_at, DateTimeType, null: false
field :name, Sträng, null: false
field :email, Sträng, null: false
fält :votes, [VoteType], null: false
fält :links, [LinkType], null: false
def länkar
Loaders::AssociationLoader.for(Användare, :länkar).load(objekt)
slut
slut
slut
Som ett resultat av att vi använder laddarna samlar vi data i batch och vi frågar efter data i två enkla sql-frågor:
Det finns också andra lösningar som löser detta problem, till exempel:
Komplexitet i förfrågningar
N + 1 frågor är inte allt, i GraphQL kan vi fritt föra över nästa attribut. Som standard är den inställd på 1. Detta kan ibland vara för mycket för servern, särskilt i en situation där vi fritt kan häcka data. Hur hanterar man det? Vi kan begränsa frågans komplexitet, men för att göra detta måste vi också ange deras kostnad i attributen. Som standard är den inställd på 1. Vi ställer in denna kostnad med hjälp av komplexitet: attribut, där vi kan mata in data: fält: länkar, [LinkType], null: false, komplexitet: 101. Om begränsningen verkligen ska fungera måste du fortfarande införa en maxgräns i ditt system:
class GraphqlTutorialSchema < GraphQL::Schema
query Typer::QueryType
mutation Typer::MutationType
använder GraphQL::Batch
max_komplexitet 100
...
slut
Spårning
GraphQL bearbetar frågor på olika sätt, och spårning är inte så enkelt om man jämför med vad vi kan göra lokalt. Tyvärr kommer rack mini profiler eller en vanlig SQL-logg inte att berätta allt för oss och kommer inte att peka på vilken del av frågan som är ansvarig för en viss tidsskiva. När det gäller GraphQL-Ruby kan vi använda kommersiella lösningar som finns tillgängliga här: https://graphql-ruby.org/queries/tracingeller försöka förbereda vår egen spårning. Nedan ser utdraget ut som en lokal tracer.
Installationen är också extremt enkel, du måste inkludera spårningsinformationen i schemat tracer (MyCustomTracer.new) konfiguration. Som i exemplet nedan:
# graphql-ruby/app/graphql/graphql_tutorial_schema.rb
class GraphqlTutorialSchema < GraphQL::Schema
query Typer::QueryType
mutation Typer::MutationType
använd GraphQL::Batch
tracer(MyCustomTracer.new)
...
slut
Utdata från en sådan spårning ser ut så här:
Startade POST "/graphql" för ::1 kl 2021-06-17 22:02:44 +0200
(0,1 ms) SELECT sqlite_version(*)
Bearbetning av GraphqlController#execute som */*
Parametrar: {"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 }n}"}}
plattform_nyckel: graphql.lex, nyckel: lex, varaktighet: 0,156 ms
platform_key: graphql.parse, nyckel: parse, varaktighet: 0,108 ms
platform_key: graphql.validate, nyckel: validate, varaktighet: 0,537 ms
plattforms_nyckel: graphql.analyze_query, nyckel: analyze_query, varaktighet: 0,123 ms
platform_key: graphql.analyze_multiplex, nyckel: analyze_multiplex, varaktighet: 0,159 ms
Laddning av länkar (0,4 ms) SELECT "links".* FROM "links"
↳ app/graphql/graphql_tutorial_schema.rb:21:in `platform_trace'
platform_key: graphql.execute_query, nyckel: execute_query, varaktighet: 15,562 ms
↳ app/graphql/laddare/record_loader.rb:12:in `perform'
↳ app/graphql/laddare/association_loader.rb:46:in `preload_association'
platform_key: graphql.execute_query_lazy, nyckel: execute_query_lazy, varaktighet: 14,12 ms
plattforms_nyckel: graphql.execute_multiplex, nyckel: execute_multiplex, varaktighet: 31,11 ms
Avslutad 200 OK på 48 ms (Views: 1,2 ms | ActiveRecord: 2,0 ms | Allokeringar: 40128)
Sammanfattning
GraphQL är inte längre en ny teknik, men lösningarna på dess problem är inte helt standardiserade om de inte ingår i biblioteket. Implementeringen av denna teknik i projekt ger många möjligheter att interagera med frontend och jag anser personligen att det är en ny kvalitet i förhållande till vad REST API erbjuder.