GraphQL, tāpat kā jebkurai citai tehnoloģijai, ir savas problēmas, no kurām dažas tieši izriet no arhitektūras, bet dažas ir identiskas tām, kas sastopamas jebkurā citā lietojumprogrammā. Tomēr risinājumi ir pilnīgi atšķirīgi.
Lai parādītu problēmu, pieņemsim šādu lietojumprogrammas arhitektūru:
Un šeit atbilstošais vaicājums GraphQL lai lejupielādētu dati. Mēs iegūstam visas saites, kā arī sistēmā pievienoto plakātu un tā saites,
{
allLinks {
id
url
apraksts
createdAt
postBy {
id
nosaukums
saites {
id
}
}
}
}
Kā parādīts tālāk, šeit redzama klasiskā n + 1 problēma ar attiecībām.
Saites ielāde (0,4ms) SELECT "links".* FROM "links" ORDER BY created_at DESC
↳ app/controllers/graphql_controller.rb:5:in `execute'
Lietotāja ielāde (0.3ms) SELECT "users".* FROM "users" WHERE "users". "id" = ? LIMIT ? [[["id", 40], ["LIMIT", 1]]]
↳ app/controllers/graphql_controller.rb:5:in `execute'
Saites ielāde (0,3 ms) SELECT "links".* FROM "links" WHERE "links". "user_id" = ? [[["user_id", 40]]]
↳ app/controllers/graphql_controller.rb:5:in `execute'
Lietotāja ielāde (0.1ms) SELECT "users".* FROM "users" WHERE "users". "id" = ? LIMIT ? [[["id", 38], ["LIMIT", 1]]]
↳ app/controllers/graphql_controller.rb:5:in `execute'
Saites ielāde (0.1ms) SELECT "links".* FROM "links" WHERE "links". "user_id" = ? [[["user_id", 38]]]
↳ app/controllers/graphql_controller.rb:5:in `execute'
Lietotāja ielāde (0.2ms) SELECT "users".* FROM "users" WHERE "users". "id" = ? LIMIT ? [[["id", 36], ["LIMIT", 1]]]
↳ app/controllers/graphql_controller.rb:5:in `execute'
Saites ielāde (0.1ms) SELECT "links".* FROM "links" WHERE "links". "user_id" = ? [[["user_id", 36]]]
↳ app/controllers/graphql_controller.rb:5:in `execute'
Lietotāja ielāde (0.1ms) SELECT "users".* FROM "users" WHERE "users". "id" = ? LIMIT ? [[["id", 34], ["LIMIT", 1]]]
↳ app/controllers/graphql_controller.rb:5:in `execute'
Saites ielāde (0,2 ms) SELECT "links".* FROM "links" WHERE "links". "user_id" = ? [[["user_id", 34]]]
↳ app/controllers/graphql_controller.rb:5:in `execute'
Lietotāja ielāde (0.1ms) SELECT "users".* FROM "users" WHERE "users". "id" = ? LIMIT ? [[["id", 32], ["LIMIT", 1]]]
Šajā gadījumā tas darbojas tieši tāpat kā šis gabals kods: Link.all.map(&:user).map(&:links).
Šķiet, ka mēs zinām problēmas risinājumu: Link.includes(user: :links).map(&:user).map(&:links), bet vai tas patiešām darbosies? Pārbaudīsim to!
Lai pārliecinātos par labojumu, es mainīju GraphQL vaicājumu, lai izmantotu tikai dažus laukus un nekādas saistības.
{
allLinks {
id
url
apraksts
createdAt
}
}
Diemžēl rezultāts rāda, ka, neraugoties uz to, ka trūkst saikņu saistībā ar lietotāju un tā saitēm, mēs joprojām pievienojam šos datus datubāzes vaicājumam. Diemžēl tie ir lieki, un ar vēl sarežģītāku struktūru tas izrādās vienkārši neefektīvi.
In GraphQL, šādas problēmas tiek risinātas citādi,vienkārši ielādējot datus partijās, pieņemot, ka dati ir nepieciešami, kad tie tiek ievadīti vaicājumā. Tā ir šāda slinka ielāde. Viena no populārākajām bibliotēkām ir https://github.com/Shopify/graphql-batch/.
Diemžēl tā uzstādīšana nav tik ērta, kā varētu šķist. Datu ielādētāji ir pieejami šeit: https://github.com/Shopify/graphql-batch/tree/master/examples, es domāju. RecordLoader klases unAssociationLoader klase. Ļaujiet mums klasiski instalēt gem 'graphql-batch' bibliotēku un pēc tam pievienot to mūsu shēmai, kā arī ielādētājus:
# graphql-ruby/app/graphql/graphql_tutorial_schema.rb
klase GraphqlTutorialSchema < GraphQL::Schema
query Types::QueryType
mutation Types::MutationType
use GraphQL::Batch
...
end
Un mūsu veidi:
# graphql-ruby/app/graphql/types/link_type.rb
modulis Types
klase LinkType < BaseNode
field :created_at, DateTimeType, null: false
field :url, String, null: false
field :description, String, null: false
lauks :post_by, UserType, null: false, metode: :user
lauks :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
klase UserType < BaseNode
field :created_at, DateTimeType, null: false
field :name, String, null: false
lauks :email, String, null: false
lauks :votes, [VoteType], null: false
lauks :links, [LinkType], null: false
def links
Loaders::AssociationLoader.for(User, :links).load(object)
end
end
end
Izmantojot iekrāvēji, mēs veicam datu paketi un meklējam datus divos vienkāršos sql vaicājumos:
N + 1 vaicājumi nav viss, jo GraphQL mēs varam brīvi pārnest uz nākamajiem atribūtiem. Pēc noklusējuma tas ir iestatīts uz 1. Tas dažkārt serverim var būt pārāk daudz, īpaši situācijā, kad varam brīvi ievietot datus. Kā ar to tikt galā? Mēs varam ierobežot vaicājuma sarežģītību, bet, lai to izdarītu, mums atribūtos jānorāda arī to izmaksas. Pēc noklusējuma tā ir iestatīta uz 1. Mēs iestatām šīs izmaksas, izmantojot sarežģītība: atribūtu, kurā varam ievadīt datus: lauks: links, [LinkType], null: false, sarežģītība: 101. Lai ierobežošana patiešām darbotos, jūsu shēmā joprojām ir jāievieš maksimālais ierobežojums:
klase GraphqlTutorialSchema < GraphQL::Schema
query Types::QueryType
mutation Types::MutationType
use GraphQL::Batch
max_complexity 100
...
beigas
Izsekošana
GraphQL pieprasījumus apstrādā atšķirīgi, un izsekošana nav tik vienkārša, ja salīdzina ar to, ko mēs varam izdarīt lokāli. Diemžēl plaukts mini profilētājs vai parasts SQL žurnāls nepastāstīs. mums visu un nenorādīs, kura vaicājuma daļa ir atbildīga par konkrēto laika nogriezni. GraphQL-Ruby gadījumā mēs varam izmantot šeit pieejamos komerciālos risinājumus: https://graphql-ruby.org/queries/tracing, vai mēģināt sagatavot savu izsekojamību. Zemāk dotais fragments izskatās kā vietējais trasētājs.
Uzstādīšana ir arī ļoti vienkārša, jums ir jāiekļauj trasēšanas informācija shēmā. trasētājs (MyCustomTracer.new) konfigurācija. Kā tālāk dotajā piemērā:
# graphql-ruby/app/graphql/graphql_tutorial_schema.rb
klase GraphqlTutorialSchema < GraphQL::Schema
query Types::QueryType
mutation Types::MutationType
use GraphQL::Batch
tracer(MyCustomTracer.new)
...
beigas
Šādas izsekošanas rezultāts izskatās šādi:
Sākts POST "/graphql" ::1 2021-06-17 22:02:44 +0200
(0.1ms) SELECT sqlite_version(*)
Apstrāde ar GraphqlController#execute kā */*
Parametri: {"query"=>"{n allLinks {n idn urln descriptionn createdAtn postBy {n idn named links {n idn }n }n }n }n}", "graphql"=>{{"query"=>"{n allLinks {n idn urln descriptionn createdAtn postBy {n idn named links {n idn }n }n }n}"}}}
platform_key: graphql.lex, key: lex, duration: 0.156 ms
platform_key: graphql.parse, atslēga: parse, ilgums: 0.108 ms
platform_key: graphql.validate, atslēga: validate, ilgums: 0.537 ms
platform_key: graphql.analyze_query, atslēga: analyze_query, ilgums: 0.123 ms
platform_key: graphql.analyze_multiplex, atslēga: analyze_multiplex, ilgums: 0.159 ms
Saites ielāde (0,4 ms) SELECT "links".* FROM "links"
↳ app/graphqql/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, ilgums: 31.11 ms
200 OK pabeigts 48 ms laikā (skatījumi: 1,2 ms | ActiveRecord: 2,0 ms | piešķīrumi: 40128).
Kopsavilkums
GraphQL vairs nav jauna tehnoloģija, taču tās problēmu risinājumi nav pilnībā standartizēti, ja tie nav daļa no bibliotēkas. Šīs tehnoloģijas ieviešana projekts sniedz daudz iespēju mijiedarboties ar frontend, un es personīgi uzskatu, ka tā ir jauna kvalitāte salīdzinājumā ar to, ko REST API piedāvājumi.