GraphQL har, som all annen teknologi, sine problemer. Noen av dem er et direkte resultat av arkitekturen, og andre er identiske med det vi ser i alle andre applikasjoner. Løsningene er imidlertid helt forskjellige.
For å presentere problemet, la oss anta følgende applikasjonsarkitektur:
Og her er den tilsvarende spørringen i GraphQL for å laste ned dataene. Vi henter alle lenker, sammen med plakaten og lenkene som er lagt til i systemet,
{
allLinks {
id
url
beskrivelse
createdAt
postedBy {
id
navn
lenker {
id
}
}
}
}
Som vist nedenfor, kan vi se det klassiske n + 1-problemet med relasjoner her.
Link Load (0.4ms) SELECT "links".* FROM "links" ORDER BY created_at DESC
↳ app/controllers/graphql_controller.rb:5:in `execute'
Brukerbelastning (0,3 ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 40], ["LIMIT", 1]]]
↳ app/controllers/graphql_controller.rb:5:in `execute'
Link Load (0.3ms) SELECT "links".* FROM "links" WHERE "links"."user_id" = ? [["user_id", 40]] [[["user_id", 40]]
↳ app/controllers/graphql_controller.rb:5:in `execute'
User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 38], ["LIMIT", 1]]]
↳ app/controllers/graphql_controller.rb:5:in `execute'
Link Load (0.1ms) SELECT "links".* FROM "links" WHERE "links"."user_id" = ? [["user_id", 38]] [[["user_id", 38]]
↳ app/controllers/graphql_controller.rb:5:in `execute'
User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 36], ["LIMIT", 1]]]
↳ app/controllers/graphql_controller.rb:5:in `execute'
Link Load (0.1ms) SELECT "links".* FROM "links" WHERE "links"."user_id" = ? [["user_id", 36]] [[["user_id", 36]]
↳ app/controllers/graphql_controller.rb:5:in `execute'
User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 34], ["LIMIT", 1]]]
↳ app/controllers/graphql_controller.rb:5:in `execute'
Link Load (0.2ms) SELECT "links".* FROM "links" WHERE "links"."user_id" = ? [["user_id", 34]] [[["user_id", 34]]
↳ app/controllers/graphql_controller.rb:5:in `execute'
User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 32], ["LIMIT", 1]]]
I dette tilfellet fungerer det akkurat som denne delen av kode: Link.all.map(&:user).map(&:links).
Vi ser ut til å vite løsningen på problemet: Link.includes(user: :links).map(&:user).map(&:links), men vil det virkelig fungere? La oss sjekke det ut!
For å bekrefte løsningen endret jeg GraphQL spørring for å bare bruke noen få felt og ingen relasjoner.
{
allLinks {
id
url
beskrivelse
createdAt
}
}
Dessverre viser resultatet at til tross for mangelen på lenker i forhold til brukeren og deres lenker, legger vi fortsatt disse dataene til databaseforespørselen. Dessverre er de overflødige, og med en enda mer komplisert struktur viser det seg rett og slett å være ineffektivt.
I GraphQLløses slike problemer på en annen måte, ganske enkelt ved å laste inn data i batcher, forutsatt at dataene er nødvendige når de legges inn i spørringen. Det er en slik lat lasting. Et av de mest populære bibliotekene er https://github.com/Shopify/graphql-batch/.
Dessverre er ikke installasjonen så problemfri som det kan virke. Datalasterne er tilgjengelige her: https://github.com/Shopify/graphql-batch/tree/master/examples, jeg mener RecordLoader klassen ogAssociationLoader klasse. La oss på klassisk vis installere gem 'graphql-batch' biblioteket og deretter legge det til i skjemaet vårt, samt lastere:
# graphql-ruby/app/graphql/graphql_tutorial_schema.rb
class GraphqlTutorialSchema < GraphQL::Schema
query Types::QueryType
mutation Types::MutationType
use GraphQL::Batch
...
end
Og våre typer:
# graphql-ruby/app/graphql/types/link_type.rb
modul Typer
class LinkType < BaseNode
field :created_at, DateTimeType, null: false
field :url, String, null: false
field :description, String, null: false
field :posted_by, UserType, null: false, method: :user
field :votes, [Types::VoteType], null: false
def bruker
Loaders::RecordLoader.for(User).load(object.user_id)
end
end
end
# graphql-ruby/app/graphql/types/user_type.rb
modul Types
class UserType < BaseNode
field :created_at, DateTimeType, null: false
field :name, String, null: false
field :email, String, null: false
field :votes, [VoteType], null: false
field :links, [LinkType], null: false
def lenker
Loaders::AssociationLoader.for(User, :links).load(object)
end
end
end
Ved hjelp av innlasterne kan vi samle inn data i grupper og søke etter data i to enkle sql-spørringer:
N + 1-spørringer er ikke alt, i GraphQL kan vi fritt overføre de neste attributtene. Som standard er den satt til 1. Dette kan noen ganger være for mye for serveren, spesielt i en situasjon der vi fritt kan hekke data. Hvordan håndtere det? Vi kan begrense kompleksiteten i spørringen, men for å gjøre dette må vi også spesifisere kostnadene i attributtene. Som standard er den satt til 1. Vi setter denne kostnaden ved hjelp av kompleksitet: attributtet, der vi kan legge inn data: field: links, [LinkType], null: false, complexity: 101. Hvis begrensningen faktisk skal fungere, må du likevel innføre en maksimumsgrense i ordningen din:
class GraphqlTutorialSchema < GraphQL::Schema
query Types::QueryType
mutation Types::MutationType
bruk GraphQL::Batch
max_kompleksitet 100
...
end
Sporing
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 prøve å lage vår egen sporing. Nedenfor ser utdraget ut som en lokal tracer.
# 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(plattformnøkkel, nøkkel, varighet)
resultat
end
def plattform_feltnøkkel(type, felt)
"graphql.#{type.graphql_name}.#{field.graphql_name}"
end
def platform_authorized_key(type)
"graphql.authorized.#{type.graphql_name}"
end
def platform_resolve_type_key(type)
"graphql.resolve_type.#{type.graphql_name}"
end
def observe(plattform_nøkkel, nøkkel, varighet)
return if key == 'authorized'
puts "plattformnøkkel: #{plattformnøkkel}, nøkkel: #{nøkkel}, varighet: #{(varighet * 1000).round(5)} ms".yellow
end
end
Installasjonen er også ekstremt enkel, du trenger bare å inkludere sporingsinformasjonen i skjemaet tracer (MyCustomTracer.new) konfigurasjon. Som i eksempelet nedenfor:
# graphql-ruby/app/graphql/graphql_tutorial_schema.rb
class GraphqlTutorialSchema < GraphQL::Schema
query Types::QueryType
mutation Types::MutationType
use GraphQL::Batch
tracer(MyCustomTracer.new)
...
end
Resultatet fra en slik sporing ser slik ut:
Startet POST "/graphql" for ::1 kl 2021-06-17 22:02:44 +0200
(0.1ms) SELECT sqlite_version(*)
Behandlet av GraphqlController#execute som */*
Parametere: {"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 }n}"}}}
plattformnøkkel: graphql.lex, nøkkel: lex, varighet: 0.156 ms
platform_key: graphql.parse, key: parse, varighet: 0.108 ms
platform_key: graphql.validate, key: validate, varighet: 0,537 ms
platform_key: graphql.analyze_query, key: analyze_query, varighet: 0.123 ms
platform_key: graphql.analyze_multiplex, key: analyze_multiplex, duration: 0,159 ms
Lasting av lenker (0,4 ms) 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, varighet: 31.11 ms
Fullført 200 OK på 48 ms (Visninger: 1,2 ms | ActiveRecord: 2,0 ms | Allokeringer: 40128)
Sammendrag
GraphQL er ikke en ny teknologi lenger, men løsningene på problemene er ikke fullt ut standardiserte hvis de ikke er en del av biblioteket. Implementeringen av denne teknologien i prosjekt gir mange muligheter til å samhandle med frontend, og jeg anser det personlig som en ny kvalitet i forhold til hva REST API tilbyr.