GraphQL, kaip ir bet kuri kita technologija, turi problemų, kai kurios iš jų kyla tiesiogiai dėl architektūros, o kai kurios yra tokios pačios kaip ir bet kurioje kitoje programoje. Tačiau sprendimai yra visiškai skirtingi.
Norėdami pristatyti problemą, tarkime, kad yra tokia taikomosios programos architektūra:
Ir čia atitinkama užklausa GraphQL atsisiųsti duomenys. Atrenkame visas nuorodas kartu su plakatu ir jo nuorodomis, įtrauktomis į sistemą,
{
allLinks {
id
url
aprašymas
createdAt
postedBy {
id
pavadinimas
nuorodos {
id
}
}
}
}
Kaip parodyta toliau, čia matome klasikinę n + 1 problemą su santykiais.
Nuorodų apkrovimas (0,4 ms) SELECT "links".* FROM "links" ORDER BY created_at DESC
↳ app/controllers/graphql_controller.rb:5:in `execute'
Naudotojo įkėlimas (0.3 ms) SELECT "users".* FROM "users" WHERE "users". "id" = ? LIMIT ? [[["id", 40], ["LIMIT", 1]]]
↳ app/controllers/graphql_controller.rb:5:in `execute'
Nuorodų įkėlimas (0,3 ms) SELECT "links".* FROM "links" WHERE "links". "user_id" = ? [[["user_id", 40]]
↳ app/controllers/graphql_controller.rb:5:in `execute'
Naudotojo įkėlimas (0,1 ms) SELECT "users".* FROM "users" WHERE "users". "id" = ? LIMIT ? [[["id", 38], ["LIMIT", 1]]]
↳ app/controllers/graphql_controller.rb:5:in `execute'
Nuorodų įkėlimas (0,1 ms) SELECT "links".* FROM "links" WHERE "links". "user_id" = ? [[["user_id", 38]]]
↳ app/controllers/graphql_controller.rb:5:in `execute'
Naudotojo įkėlimas (0,2 ms) SELECT "users".* FROM "users" WHERE "users". "id" = ? LIMIT ? [[["id", 36], ["LIMIT", 1]]]
↳ app/controllers/graphql_controller.rb:5:in `execute'
Nuorodų įkėlimas (0,1 ms) SELECT "links".* FROM "links" WHERE "links". "user_id" = ? [[["user_id", 36]]]
↳ app/controllers/graphql_controller.rb:5:in `execute'
Naudotojo įkėlimas (0,1 ms) SELECT "users".* FROM "users" WHERE "users". "id" = ? LIMIT ? [[["id", 34], ["LIMIT", 1]]]
↳ app/controllers/graphql_controller.rb:5:in `execute'
Nuorodų įkėlimas (0,2 ms) SELECT "links".* FROM "links" WHERE "links". "user_id" = ? [[["user_id", 34]]]
↳ app/controllers/graphql_controller.rb:5:in `execute'
Naudotojo įkėlimas (0,1 ms) SELECT "users".* FROM "users" WHERE "users". "id" = ? LIMIT ? [[["id", 32], ["LIMIT", 1]]]
Šiuo atveju jis veikia lygiai taip pat, kaip ir šis elementas kodas: Link.all.map(&:user).map(&:links).
Atrodo, kad žinome problemos sprendimą: Link.includes(user: :links).map(&:user).map(&:links), bet ar tai tikrai veiks? Patikrinkime!
Norėdamas patikrinti pataisymą, pakeičiau GraphQL užklausą, kad būtų naudojami tik keli laukai ir nebūtų jokių ryšių.
{
allLinks {
id
url
aprašymas
createdAt
}
}
Deja, rezultatas rodo, kad, nepaisant to, jog trūksta nuorodų, susijusių su naudotoju ir jo nuorodomis, mes vis tiek pridedame šiuos duomenis prie duomenų bazės užklausos. Deja, jie yra pertekliniai, o esant dar sudėtingesnei struktūrai, pasirodo, kad tai tiesiog neefektyvu.
Svetainėje GraphQL, tokios problemos sprendžiamos kitaip, tiesiog pakraunant duomenis partijomis, darant prielaidą, kad duomenų reikia tada, kai jie pateikiami užklausai. Tai toks tingus įkėlimas. Viena iš populiariausių bibliotekų yra https://github.com/Shopify/graphql-batch/.
Deja, jos diegimas nėra toks paprastas, kaip gali atrodyti. Duomenų įkroviklius galima rasti čia: https://github.com/Shopify/graphql-batch/tree/master/examples, turiu omenyje RecordLoader klasė irAssociationLoader klasė. Įdiekime klasikinį gem 'graphql-batch' biblioteką ir pridėti ją prie mūsų schemos, taip pat įkroviklius:
# graphql-ruby/app/graphql/graphql_tutorial_schema.rb
klasė GraphqlTutorialSchema < GraphQL::Schema
užklausa Types::QueryType
mutation Types::MutationType
use GraphQL::Batch
...
end
Ir mūsų tipai:
# graphql-ruby/app/graphql/types/link_type.rb
modulis Types
klasė LinkType < BaseNode
field :created_at, DateTimeType, null: false
laukas :url, String, null: false
field :description, String, null: false
laukas :posted_by, UserType, null: false, metodas: :user
laukas :votes, [Types::VoteType], null: false
def user
Loaders::RecordLoader.for(User).load(object.user_id)
end
end
end
# graphql-ruby/app/graphql/types/user_type.rb
modulis Types
klasė UserType < BaseNode
field :created_at, DateTimeType, null: false
laukas :name, String, null: false
laukas :email, String, null: false
laukas :votes, [VoteType], null: false
laukas :links, [LinkType], null: false
def links
Loaders::AssociationLoader.for(User, :links).load(object)
end
end
end
Naudodami įkroviklius, mes partijomis pateikiame duomenis ir užklausiame duomenų dviem paprastomis sql užklausomis:
N + 1 užklausos nėra viskas, nes GraphQL galime laisvai perkelti kitus atributus. Pagal numatytuosius nustatymus jis yra lygus 1. Kartais serveriui to gali būti per daug, ypač tais atvejais, kai galime laisvai įterpti duomenis. Kaip su tuo susidoroti? Galime apriboti užklausos sudėtingumą, tačiau tam taip pat turime atributuose nurodyti jų kainą. Pagal numatytuosius nustatymus ji nustatyta lygi 1. Šią kainą nustatome naudodami sudėtingumas: atributas, į kurį galime įvesti duomenis: laukas: links, [LinkType], null: false, sudėtingumas: 101. Jei ribojimas iš tikrųjų veikia, vis tiek reikia į savo sistemą įtraukti didžiausią ribą:
GraphQL užklausas apdoroja skirtingai, o atsekimas nėra toks paprastas, palyginti su tuo, ką galime atlikti vietoje. Deja, stovo mini profileris arba įprastas SQL žurnalas nepasakys mus viską ir nenurodys, kuri užklausos dalis yra atsakinga už tam tikrą laiko atkarpą. GraphQL-Ruby atveju galime naudoti komercinius sprendimus, kuriuos galima rasti čia: https://graphql-ruby.org/queries/tracing, arba pabandyti parengti savo atsekamąją medžiagą. Toliau pateikta fragmentas atrodo kaip vietinis sekimas.
Diegimas taip pat labai paprastas, reikia į schemą įtraukti žymeklio informaciją tracer (MyCustomTracer.new) konfigūracija. Kaip toliau pateiktame pavyzdyje:
Pradėtas POST "/graphql" ::1 2021-06-17 22:02:44 +0200
(0,1 ms) SELECT sqlite_version(*)
Apdorojimas pagal GraphqlController#execute kaip */*
Parametrai: {"query"=>"{n allLinks {n idn urln descriptionn createdAtn postedBy {n idn namen links {n idn }n }n }n }n}", "graphql"=>{{"query"=>"{n allLinks {n idn urln descriptionn createdAtn postedBy {n idn namen links {n idn }n }n }n }n}"}}}
platform_key: graphql.lex, key: lex, duration: 0.156 ms
platform_key: graphql.parse, raktas: parse, trukmė: 0,108 ms
platform_key: graphql.validate, raktas: validate, trukmė: 0.537 ms
platform_key: graphql.analyze_query, raktas: analyze_query, trukmė: 0.123 ms
platform_key: graphql.analyze_multiplex, raktas: analyze_multiplex, trukmė: 0.159 ms
Nuorodų apkrovimas (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
Užbaigta 200 OK per 48 ms (Peržiūros: 1,2 ms | ActiveRecord: 2,0 ms | Alokacijos: 40128)
Santrauka
GraphQL nebėra nauja technologija, tačiau jos problemų sprendimai nėra visiškai standartizuoti, jei jie nėra bibliotekos dalis. Šios technologijos įgyvendinimas projektas suteikia daug galimybių sąveikauti su frontend'u, ir aš asmeniškai manau, kad tai yra nauja kokybė, palyginti su tuo, ką REST API pasiūlymai.