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:
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 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 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.