GraphQL har som enhver anden teknologi sine problemer, og nogle af dem er et direkte resultat af arkitekturen, mens andre er identiske med det, vi ser i enhver anden applikation. Men løsningerne er helt forskellige.
For at præsentere problemet, lad os antage følgende applikationsarkitektur:
Og her er den tilsvarende forespørgsel i GraphQL for at downloade data. Vi henter alle links sammen med plakaten og dens links, der er tilføjet til systemet,
{
allLinks {
id
url
beskrivelse
createdAt
postedBy {
id
navn
links {
id
}
}
}
}
Som vist nedenfor kan vi se det klassiske n + 1-problem med relationer her.
Indlæsning af links (0,4 ms) SELECT "links".* FROM "links" ORDER BY created_at DESC
↳ app/controllers/graphql_controller.rb:5:in `execute'
Brugerindlæsning (0,3 ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 40], ["LIMIT", 1]]
↳ app/controllers/graphql_controller.rb:5:in `execute'
Indlæsning af link (0,3 ms) SELECT "links".* FROM "links" WHERE "links"."user_id" = ? [["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'
Indlæsning af link (0,1 ms) SELECT "links".* FROM "links" WHERE "links"."user_id" = ? [["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'
Indlæsning af link (0,1 ms) SELECT "links".* FROM "links" WHERE "links"."user_id" = ? [["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'
Indlæsning af link (0,2 ms) SELECT "links".* FROM "links" WHERE "links"."user_id" = ? [["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 tilfælde fungerer det nøjagtigt som dette stykke Kode: Link.all.map(&:user).map(&:links).
Det ser ud til, at vi kender løsningen på problemet: Link.includes(user: :links).map(&:user).map(&:links)Men virker det virkelig? Lad os tjekke det ud!
For at bekræfte løsningen ændrede jeg GraphQL forespørgsel til kun at bruge nogle få felter og ingen relation.
{
allLinks {
id
url
beskrivelse
oprettetAt
}
}
Desværre viser resultatet, at vi på trods af manglen på links i forhold til brugeren og deres links stadig vedhæfter disse data til databaseforespørgslen. Desværre er de overflødige, og med en endnu mere kompliceret struktur viser det sig simpelthen at være ineffektivt.
I GraphQLSådanne problemer løses på en anden måde, simpelthen ved at indlæse data i batches, idet man antager, at der er brug for dataene, når de sættes ind i forespørgslen. Det er sådan en doven indlæsning. Et af de mest populære biblioteker er https://github.com/Shopify/graphql-batch/.
Desværre er installationen ikke så problemfri, som det måske ser ud til. Data loaderne er tilgængelige her: https://github.com/Shopify/graphql-batch/tree/master/examples, jeg mener RecordLoader klassen ogAssociationLoader klasse. Lad os på klassisk vis installere gem 'graphql-batch' bibliotek og derefter tilføje det til vores schema samt loadere:
# graphql-ruby/app/graphql/graphql_tutorial_schema.rb
class GraphqlTutorialSchema < GraphQL::Schema
forespørgsel Types::QueryType
mutation Typer::MutationType
brug GraphQL::Batch
...
end
Og vores typer:
# graphql-ruby/app/graphql/types/link_type.rb
modul Typer
klasse 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 bruger
Loaders::RecordLoader.for(User).load(object.user_id)
end
end
end
# graphql-ruby/app/graphql/types/user_type.rb
modul Typer
klassen 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 links
Loaders::AssociationLoader.for(User, :links).load(object)
end
end
slut
Som et resultat af brugen af loaderne samler vi data, og vi søger efter data i to enkle sql-forespørgsler:
Der er også andre løsninger, der løser dette problem, f.eks:
Forespørgslernes kompleksitet
N + 1 forespørgsler er ikke alt, i GraphQL kan vi frit overføre de næste attributter. Som standard er den sat til 1. Det kan nogle gange være for meget for serveren, især i en situation, hvor vi frit kan indlejre data. Hvordan håndterer man det? Vi kan begrænse forespørgslens kompleksitet, men for at gøre det skal vi også angive deres omkostninger i attributterne. Som standard er den sat til 1. Vi indstiller denne omkostning ved hjælp af kompleksitet: attribut, hvor vi kan indtaste data: field: links, [LinkType], null: false, complexity: 101. Hvis begrænsningen rent faktisk skal fungere, skal du stadig indføre en maksimumsgrænse i din ordning:
class GraphqlTutorialSchema < GraphQL::Schema
query Typer::QueryType
mutation Types::MutationType
brug GraphQL::Batch
max_kompleksitet 100
...
slut
Sporing
GraphQL behandler forespørgsler forskelligt, og sporing er ikke så enkelt, hvis man sammenligner med, hvad vi kan gøre lokalt. Desværre vil rack mini profiler eller en almindelig SQL-log ikke fortælle os alt og vil ikke pege på, hvilken del af forespørgslen der er ansvarlig for et givet tidsinterval. Når det gælder GraphQL-Ruby, kan vi bruge kommercielle løsninger, der er tilgængelige her: https://graphql-ruby.org/queries/tracingeller prøve at forberede vores egen sporing. Nedenfor ser uddraget ud som en lokal tracer.
Installationen er også ekstremt enkel, du skal blot inkludere tracer-oplysningerne i skemaet tracer (MyCustomTracer.new) konfiguration. Som i eksemplet nedenfor:
# graphql-ruby/app/graphql/graphql_tutorial_schema.rb
class GraphqlTutorialSchema < GraphQL::Schema
forespørgsel Types::QueryType
mutation Typer::MutationType
brug GraphQL::Batch
tracer(MyCustomTracer.new)
...
slut
Resultatet af en sådan sporing ser sådan ud:
Startede POST "/graphql" for ::1 den 2021-06-17 22:02:44 +0200
(0.1ms) SELECT sqlite_version(*)
Behandlet af GraphqlController#execute som */*
Parametre: {"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}"}}
platform_key: graphql.lex, key: lex, duration: 0.156 ms
platform_key: graphql.parse, key: parse, duration: 0.108 ms
platform_key: graphql.validate, key: validate, duration: 0.537 ms
platform_key: graphql.analyze_query, key: analyze_query, duration: 0.123 ms
platform_key: graphql.analyze_multiplex, key: analyze_multiplex, duration: 0.159 ms
Indlæsning af links (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, duration: 31.11 ms
Afsluttet 200 OK på 48 ms (Views: 1,2 ms | ActiveRecord: 2,0 ms | Allokeringer: 40128)
Sammenfatning
GraphQL er ikke længere en ny teknologi, men løsningerne på dens problemer er ikke fuldt ud standardiserede, hvis de ikke er en del af biblioteket. Implementeringen af denne teknologi i projekt giver mange muligheder for at interagere med frontend, og jeg anser det personligt for at være en ny kvalitet i forhold til, hvad REST API tilbyder.