GraphQL:llä, kuten millä tahansa teknologialla, on ongelmansa, joista osa johtuu suoraan arkkitehtuurista ja osa on samanlaisia kuin missä tahansa muussa sovelluksessa. Ratkaisut ovat kuitenkin täysin erilaisia.
Ongelman esittelemiseksi oletetaan seuraava sovellusarkkitehtuuri:
Ja tässä vastaava kysely osoitteessa GraphQL tietojen lataamiseksi. Haemme kaikki linkit sekä järjestelmään lisätyn julisteen ja sen linkit,
{
allLinks {
id
url
description
createdAt
postedBy {
id
name
linkit {
id
}
}
}
}
Kuten alla on esitetty, voimme nähdä klassisen n + 1 -ongelman suhteiden kanssa.
Linkkien kuormitus (0.4ms) SELECT "links".* FROM "links" ORDER BY created_at DESC
↳ app/controllers/graphql_controller.rb:5:in `toteuta'
Käyttäjän lataus (0.3ms) SELECT "users".* FROM "users" WHERE "users". "id" = ? LIMIT ? [["id", 40], ["LIMIT", 1]]
↳ app/controllers/graphql_controller.rb:5:in `execute' (suorita)
Linkkien lataus (0.3ms) SELECT "links".* FROM "links" WHERE "links". "user_id" = ? [["user_id", 40]]
↳ app/controllers/graphql_controller.rb:5:in 'suorita'
Käyttäjän lataus (0.1ms) SELECT "users".* FROM "users" WHERE "users". "id" = ? LIMIT ? [[["id", 38], ["LIMIT", 1]]
↳ app/controllers/graphql_controller.rb:5:in `execute' (suorita)
Linkkien lataus (0.1ms) SELECT "links".* FROM "links" WHERE "links". "user_id" = ? [["user_id", 38]]
↳ app/controllers/graphql_controller.rb:5:in 'suorita'
Käyttäjän lataus (0.2ms) SELECT "users".* FROM "users" WHERE "users". "id" = ? LIMIT ? [[["id", 36], ["LIMIT", 1]]
↳ app/controllers/graphql_controller.rb:5:in `execute' (suorita)
Linkkien lataus (0.1ms) SELECT "links".* FROM "links" WHERE "links". "user_id" = ? [["user_id", 36]]
↳ app/controllers/graphql_controller.rb:5:in 'suorita'
Käyttäjän lataus (0.1ms) SELECT "users".* FROM "users" WHERE "users". "id" = ? LIMIT ? [[["id", 34], ["LIMIT", 1]]
↳ app/controllers/graphql_controller.rb:5:in `execute' (suorita)
Linkkien lataus (0.2ms) SELECT "links".* FROM "links" WHERE "links". "user_id" = ? [["user_id", 34]]
↳ app/controllers/graphql_controller.rb:5:in 'suorita'
Käyttäjän lataus (0.1ms) SELECT "users".* FROM "users" WHERE "users". "id" = ? LIMIT ? [[["id", 32], ["LIMIT", 1]]
Tässä tapauksessa se toimii täsmälleen kuten tämä pala koodi: Link.all.map(&:user).map(&:links).
Näyttää siltä, että tiedämme ratkaisun ongelmaan: Link.includes(user: :links).map(&:user).map(&:links), mutta toimiiko se todella? Tarkistetaan se!
Varmistaakseni korjauksen, muutin GraphQL kysely, jossa käytetään vain muutamaa kenttää eikä mitään suhdetta.
{
allLinks {
id
url
description
createdAt
}
}
Valitettavasti tulos osoittaa, että vaikka käyttäjään ja hänen linkkeihinsä liittyvät linkit puuttuvat, liitämme silti nämä tiedot tietokantakyselyyn. Valitettavasti ne ovat tarpeettomia, ja vielä monimutkaisemman rakenteen kanssa se osoittautuu yksinkertaisesti tehottomaksi.
Osoitteessa GraphQLtällaiset ongelmat ratkaistaan eri tavalla, yksinkertaisesti lataamalla tiedot erissä olettaen, että tietoja tarvitaan silloin, kun ne asetetaan kyselyyn. Se on tällainen laiska lataus. Yksi suosituimmista kirjastoista on https://github.com/Shopify/graphql-batch/.
Valitettavasti sen asennus ei ole niin vaivatonta kuin miltä se saattaa vaikuttaa. Tiedonlataajat ovat saatavilla täältä: https://github.com/Shopify/graphql-batch/tree/master/examples, tarkoitan siis RecordLoader luokka jaAssociationLoader luokka. Asennetaan klassisesti gem 'graphql-batch' kirjastoa ja lisää se sitten skeemaamme, samoin kuin lataajat:
# graphql-ruby/app/graphql/graphql_tutorial_schema.rb
class GraphqlTutorialSchema < GraphQL::Schema
query Types::QueryType
mutation Types::MutationType
use GraphQL::Batch
...
end
Ja meidän tyyppimme:
# graphql-ruby/app/graphql/types/link_type.rb
moduuli Types
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 user
Loaders::RecordLoader.for(User).load(object.user_id)
end
end
end
# graphql-ruby/app/graphql/types/user_type.rb
moduuli Types
class UserType < BaseNode
field :created_at, DateTimeType, null: false
kenttä :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
end
Lataajien käytön tuloksena keräämme tiedot eräajona ja haemme tietoja kahdella yksinkertaisella sql-kyselyllä:
On myös muita ratkaisuja, jotka ratkaisevat tämän ongelman, kuten:
Kyselyjen monimutkaisuus
N + 1 kyselyt eivät ole kaikki, vuonna GraphQL voimme vapaasti siirtää seuraavat ominaisuudet. Oletusarvo on 1. Tämä voi joskus olla liikaa palvelimelle, erityisesti tilanteessa, jossa voimme vapaasti sijoittaa tietoja. Miten käsitellä sitä? Voimme rajoittaa kyselyn monimutkaisuutta, mutta tätä varten meidän on myös määritettävä niiden kustannukset attribuuteissa. Oletuksena se on asetettu arvoon 1. Asetamme tämän kustannuksen käyttämällä monimutkaisuus: attribuutti, johon voimme syöttää tietoja: field: links, [LinkType], null: false, complexity: 101. Jos rajoittamisen on tarkoitus todella toimia, sinun on silti otettava käyttöön enimmäisraja järjestelmässäsi:
class GraphqlTutorialSchema < GraphQL::Schema
query Types::QueryType
mutation Types::MutationType
use GraphQL::Batch
max_complexity 100
...
end
Jäljitys
GraphQL käsittelee kyselyitä eri tavalla, eikä jäljittäminen ole niin yksinkertaista, jos sitä verrataan siihen, mitä voimme tehdä paikallisesti. Valitettavasti rack mini profiler tai tavallinen SQL-loki ei kerro kaikkea eikä osoita, mikä osa kyselystä on vastuussa tietystä aikaviipaleesta. GraphQL-Rubyn tapauksessa voimme käyttää kaupallisia ratkaisuja, jotka ovat saatavilla täällä: https://graphql-ruby.org/queries/tracingtai yrittää valmistella omaa jäljitystä. Alla oleva pätkä näyttää paikalliselta jäljittäjältä.
# 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' => 'graphql.execute_query_lazy' (suorita_kysely_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(platform_key, key, duration)
tulos
end
def platform_field_key(type, field)
"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(platform_key, key, duration)
return if key == 'authorized'
puts "platform_key: #{platform_key}, key: #{key}, duration: #{(duration * 1000).round(5)} ms".yellow
end
end
Asennus on myös erittäin yksinkertaista, sinun täytyy sisällyttää jäljittimen tiedot skeemaan. tracer (MyCustomTracer.new) kokoonpano. Kuten alla olevassa esimerkissä:
# 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
Tällaisen jäljityksen tulos näyttää tältä:
Käynnistetty POST "/graphql" for ::1 klo 2021-06-17 22:02:44 +0200
(0.1ms) SELECT sqlite_version(*)
Käsittelee GraphqlController#execute as */*
Parametrit: {"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 }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, avain: analyze_query, kesto: 0.123 ms
platform_key: graphql.analyze_multiplex, avain: analyze_multiplex, kesto: 0.159 ms
Linkkien kuormitus (0.4ms) SELECT "links".* FROM "links" (linkit)
↳ app/graphql/graphql_tutorial_schema.rb:21:in `platform_trace' (alustan jäljitys)
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
Suoritettu 200 OK 48 ms:ssa (Views: 1.2ms | ActiveRecord: 2.0ms | Allocations: 40128).
Yhteenveto
GraphQL ei ole enää uusi tekniikka, mutta sen ongelmien ratkaisut eivät ole täysin standardoituja, jos ne eivät ole osa kirjastoa. Tämän tekniikan toteuttaminen projekti antaa paljon mahdollisuuksia vuorovaikutukseen etusivun kanssa, ja itse pidän sitä uutena ominaisuutena verrattuna siihen, mitä REST API tarjoaa.