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:
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 processes queries differently, and tracing is not that simple if compares to what we can do locally. Unfortunately, the rack mini profiler or a regular SQL log will not tell us everything and will not point which part of the query is responsible for a given time slice. In the case of GraphQL-Ruby, we can use commercial solutions available here: 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.