GraphQLil, nagu igal tehnoloogial, on omad probleemid, millest mõned tulenevad otseselt arhitektuurist ja mõned on identsed sellega, mida me näeme mis tahes muu rakenduse puhul. Lahendused on aga täiesti erinevad.
Probleemi tutvustamiseks oletame, et rakenduse arhitektuur on järgmine:
Ja siin on vastav päring GraphQL andmete allalaadimiseks. Me toome kõik lingid koos plakatiga ja selle lingid lisatakse süsteemi,
{
allLinks {
id
url
description
createdAt
postedBy {
id
name
lingid {
id
}
}
}
}
Nagu allpool näidatud, näeme siin klassikalist n + 1 suhetega probleemi.
Linkide laadimine (0.4ms) SELECT "links".* FROM "links" ORDER BY created_at DESC
↳ app/controllers/graphql_controller.rb:5:in `execute'
User Load (0.3ms) SELECT "users".* FROM "users" WHERE "users". "id" = ? LIMIT ? [["id", 40], ["LIMIT", 1]]
↳ app/controllers/graphql_controller.rb:5:in `execute'
Linkide laadimine (0.3ms) 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'
Linkide laadimine (0.1ms) SELECT "links".* FROM "links" WHERE "links". "user_id" = ? [["user_id", 38]]
↳ app/controllers/graphql_controller.rb:5:in `execute'
Kasutajate laadimine (0.2ms) SELECT "users".* FROM "users" WHERE "users". "id" = ? LIMIT ? [["id", 36], ["LIMIT", 1]]
↳ app/controllers/graphql_controller.rb:5:in `execute'
Linkide laadimine (0.1ms) SELECT "links".* FROM "links" WHERE "links". "user_id" = ? [["user_id", 36]]
↳ app/controllers/graphql_controller.rb:5:in `execute'
Kasutajate laadimine (0.1ms) SELECT "users".* FROM "users" WHERE "users". "id" = ? LIMIT ? [["id", 34], ["LIMIT", 1]]
↳ app/controllers/graphql_controller.rb:5:in `execute'
Linkide laadimine (0.2ms) 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]]
Sellisel juhul töötab see täpselt nagu see tükk kood: Link.all.map(&:user).map(&:links).
Tundub, et me teame probleemi lahendust: Link.includes(user: :links).map(&:user).map(&:links), kuid kas see tõesti toimib? Kontrollime seda!
Selleks, et kontrollida parandust, muutsin ma GraphQL päring, et kasutada ainult mõnda välja ja mingit seost.
{
allLinks {
id
url
description
createdAt
}
}
Kahjuks näitab tulemus, et hoolimata linkide puudumisest seoses kasutaja ja nende linkidega, lisame need andmed ikkagi andmebaasi päringule. Kahjuks on need üleliigsed ja veelgi keerulisema struktuuri puhul osutub see lihtsalt ebaefektiivseks.
Veebilehel GraphQL, lahendatakse sellised probleemid teisiti,lihtsalt laadides andmeid partiidena, eeldades, et andmeid on vaja siis, kui need päringusse pannakse. See on selline laisk laadimine. Üks populaarsemaid raamatukogusid on https://github.com/Shopify/graphql-batch/.
Kahjuks ei ole selle paigaldamine nii lihtne, kui see võib tunduda. Andmete laadijad on saadaval siin: https://github.com/Shopify/graphql-batch/tree/master/examples, ma mõtlen RecordLoader klassi jaAssociationLoader klass. Paigaldame klassikaliselt gem 'graphql-batch' raamatukogu ja seejärel lisame selle meie skeemi, samuti laadijad:
# graphql-ruby/app/graphql/graphql_tutorial_schema.rb
class GraphqlTutorialSchema < GraphQL::Schema
query Types::QueryType
mutation Types::MutationType
use GraphQL::Batch
...
end
Ja meie tüübid:
# graphql-ruby/app/graphql/types/link_type.rb
moodul Tüübid
class LinkType < BaseNode
field :created_at, DateTimeType, null: false
field :url, String, null: false
field :description, String, null: false
väli :posted_by, UserType, null: false, meetod: :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
moodul Tüübid
class UserType < BaseNode
väli :created_at, DateTimeType, null: false
väli :name, String, null: false
field :email, String, null: false
väli :votes, [VoteType], null: false
field :links, [LinkType], null: false
def links
Loaders::AssociationLoader.for(User, :links).load(object)
end
end
end
Laadijate kasutamise tulemusena pakime andmeid partiidena ja küsime andmeid kahe lihtsa sql päringuga:
N + 1 päringud ei ole kõik, in GraphQL saame vabalt kanda üle järgmised atribuudid. Vaikimisi on see seatud 1. See võib mõnikord olla serverile liiga palju, eriti olukorras, kus me saame andmeid vabalt pesitseda. Kuidas sellega toime tulla? Me saame piirata päringu keerukust, kuid selleks peame määrama ka nende maksumuse atribuutides. Vaikimisi on see määratud 1. Me määrame selle kulu, kasutades keerukus: atribuut, kuhu me saame sisestada andmeid: field: links, [LinkType], null: false, complexity: 101. Kui piiramine peaks tegelikult toimima, peate ikkagi kehtestama oma skeemi maksimumpiiri:
class GraphqlTutorialSchema < GraphQL::Schema
päring Tüübid::QueryType
mutation Types::MutationType
use GraphQL::Batch
max_complexity 100
...
end
Jälgimine
GraphQL töötleb päringuid erinevalt ja jälgimine ei ole nii lihtne, kui võrrelda seda sellega, mida me saame teha kohapeal. Kahjuks ei ütle rack mini profiler või tavaline SQL log meile kõike ja ei osuta, milline osa päringust on vastutav antud ajaviilu eest. GraphQL-Ruby puhul saame kasutada siin kättesaadavaid kommertslahendusi: https://graphql-ruby.org/queries/tracing, või püüda valmistada ise oma jälgimist. Järgnevalt näeb see lõik välja nagu kohalik tracer.
# 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'
}
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)
tulemus
end
def platform_field_key(type, field)
"graphql.#{tüüp.graphql_nimi}.#{välja.graphql_nimi}"
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: #{(kestus * 1000).round(5)} ms".yellow
end
end
Paigaldamine on samuti väga lihtne, peate lisama jälgimisseadme andmed skeemi tracer (MyCustomTracer.new) konfiguratsioon. Nagu alljärgnevas näites:
# 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
Sellise jälgimise väljund näeb välja selline:
Käivitati POST "/graphql" jaoks ::1 kell 2021-06-17 22:02:44 +0200
(0.1ms) SELECT sqlite_version(*)
Töötlemine GraphqlController#ulemusena */*
Parameetrid: {"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 }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, võti: analyze_query, kestus: 0.123 ms
platform_key: graphql.analyze_multiplex, key: analyze_multiplex, duration: 0.159 ms
Linkide koormus (0.4ms) 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
Lõpetatud 200 OK 48 ms jooksul (Vaated: 1.2ms | ActiveRecord: 2.0ms | Allocations: 40128)
Kokkuvõte
GraphQL ei ole enam uus tehnoloogia, kuid selle probleemide lahendused ei ole täielikult standardiseeritud, kui need ei ole osa raamatukogust. Selle tehnoloogia rakendamise projekt annab palju võimalusi frontendiga suhtlemiseks ja mina isiklikult pean seda uueks kvaliteediks võrreldes sellega, mida REST API pakub.