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:
Det finnes også andre løsninger som løser dette problemet, for eksempel:
Kompleksiteten i spørringene
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 behandler spørringer ulikt, og sporing er ikke så enkelt sammenlignet med hva vi kan gjøre lokalt. Dessverre vil ikke rack mini profiler eller en vanlig SQL-logg fortelle oss alt, og den vil ikke vise hvilken del av spørringen som er ansvarlig for et gitt tidsavsnitt. Når det gjelder GraphQL-Ruby, kan vi bruke kommersielle løsninger som er tilgjengelige her: 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.