Jako každá technologie má i GraphQL své problémy, z nichž některé vyplývají přímo z architektury a některé jsou totožné s problémy, které se vyskytují v jakékoli jiné aplikaci. Jejich řešení jsou však zcela odlišná.
Pro představení problému předpokládejme následující architekturu aplikace:
A zde je odpovídající dotaz v GraphQL stáhnout data. Stáhneme všechny odkazy spolu s plakátem a jeho odkazy přidanými do systému,
{
allLinks {
id
url
description
createdAt
postedBy {
id
name
links {
id
}
}
}
}
Jak je zobrazeno níže, vidíme zde klasický problém n + 1 se vztahy.
Zatížení odkazů (0,4 ms) SELECT "links".* FROM "links" ORDER BY created_at DESC
↳ app/controllers/graphql_controller.rb:5:in `execute'
Načítání uživatelů (0.3ms) SELECT "users".* FROM "users" WHERE "users". "id" = ? LIMIT ? [["id", 40], ["LIMIT", 1]]
↳ app/controllers/graphql_controller.rb:5:in `execute'
Načítání odkazů (0.3ms) SELECT "links".* FROM "links" WHERE "links". "user_id" = ? [["user_id", 40]]
↳ app/controllers/graphql_controller.rb:5:in `execute'
Načítání uživatelů (0.1ms) SELECT "users".* FROM "users" WHERE "users". "id" = ? LIMIT ? [["id", 38], ["LIMIT", 1]]
↳ app/controllers/graphql_controller.rb:5:in `execute'
Načítání odkazů (0.1ms) SELECT "links".* FROM "links" WHERE "links". "user_id" = ? [["user_id", 38]]
↳ app/controllers/graphql_controller.rb:5:in `execute'
Načítání uživatelů (0.2ms) SELECT "users".* FROM "users" WHERE "users". "id" = ? LIMIT ? [["id", 36], ["LIMIT", 1]]
↳ app/controllers/graphql_controller.rb:5:in `execute'
Načítání odkazů (0.1ms) SELECT "links".* FROM "links" WHERE "links". "user_id" = ? [["user_id", 36]]
↳ app/controllers/graphql_controller.rb:5:in `execute'
Načítání uživatelů (0.1ms) SELECT "users".* FROM "users" WHERE "users". "id" = ? LIMIT ? [["id", 34], ["LIMIT", 1]]
↳ app/controllers/graphql_controller.rb:5:in `execute'
Načítání odkazů (0.2ms) SELECT "links".* FROM "links" WHERE "links". "user_id" = ? [["user_id", 34]]
↳ app/controllers/graphql_controller.rb:5:in `execute'
Načítání uživatelů (0.1ms) SELECT "users".* FROM "users" WHERE "users". "id" = ? LIMIT ? [["id", 32], ["LIMIT", 1]]
V tomto případě to funguje přesně jako tento kus kód: Link.all.map(&:user).map(&:links).
Zdá se, že řešení problému známe: Link.includes(user: :links).map(&:user).map(&:links), ale bude to skutečně fungovat? Pojďme si to ověřit!
Pro ověření opravy jsem změnil GraphQL dotaz použít pouze několik polí a žádný vztah.
{
allLinks {
id
url
description
createdAt
}
}
Výsledek bohužel ukazuje, že i přes absenci odkazů ve vztahu k uživateli a jeho odkazům, přesto tyto údaje připojujeme k dotazu do databáze. Bohužel jsou nadbytečné a při ještě složitější struktuře se ukazuje, že je to prostě neefektivní.
Na adrese GraphQL, se takové problémy řeší jinak,jednoduše načítáním dat v dávkách za předpokladu, že data jsou potřebná v okamžiku, kdy jsou vložena do dotazu. Jedná se o takové líné načítání. Jednou z nejoblíbenějších knihoven je https://github.com/Shopify/graphql-batch/.
Bohužel jeho instalace není tak bezproblémová, jak by se mohlo zdát. Nahrávače dat jsou k dispozici zde: https://github.com/Shopify/graphql-batch/tree/master/examples, mám na mysli tzv. RecordLoader a třídaAssociationLoader třída. Nainstalujme klasicky třídu gem 'graphql-batch' a přidat ji do našeho schématu, stejně jako zavaděče:
# graphql-ruby/app/graphql/graphql_tutorial_schema.rb
class GraphqlTutorialSchema < GraphQL::Schema
query Types::QueryType
mutation Types::MutationType
use GraphQL::Batch
...
end
A naše typy:
# graphql-ruby/app/graphql/types/link_type.rb
modul Types
class LinkType < BaseNode
field :created_at, DateTimeType, null: false
pole :url, String, null: false
field :description, String, null: false
pole :posted_by, UserType, null: false, method: :user
pole :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
modul Types
class UserType < BaseNode
field :created_at, DateTimeType, null: false
pole :name, String, null: false
pole :email, String, null: false
pole :votes, [VoteType], null: false
pole :links, [LinkType], null: false
def links
Loaders::AssociationLoader.for(User, :links).load(object)
end
end
end
Díky použití zavaděčů dávkujeme data a dotazujeme se na data ve dvou jednoduchých dotazech sql:
N + 1 dotazů není všechno, v GraphQL můžeme volně přenášet další atributy. Ve výchozím nastavení je nastavena na 1. To může být někdy pro server příliš, zejména v situaci, kdy můžeme data volně vnořovat. Jak se s tím vypořádat? Můžeme omezit složitost dotazu, ale k tomu potřebujeme v atributech určit také jejich cenu. Ve výchozím nastavení je nastavena na hodnotu 1. Tuto cenu nastavíme pomocí příkazu složitost: atribut, do kterého můžeme zadávat data: pole: links, [LinkType], null: false, complexity: 101. Pokud má omezení skutečně fungovat, musíte do svého systému zavést maximální limit:
třída GraphqlTutorialSchema < GraphQL::Schema
query Types::QueryType
mutation Types::MutationType
use GraphQL::Batch
max_complexity 100
...
end
Sledování
GraphQL zpracovává dotazy odlišně a sledování není tak jednoduché, pokud ho porovnáme s tím, co můžeme dělat lokálně. Bohužel, rackový mini profiler ani běžný log SQL neřeknou. nás vše a neukáže, která část dotazu je zodpovědná za daný časový úsek. V případě jazyka GraphQL-Ruby můžeme použít komerční řešení, které je k dispozici zde: https://graphql-ruby.org/queries/tracing, nebo se pokusit připravit vlastní stopu. Níže uvedený úryvek vypadá jako místní trasování.
# 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'
}
def platform_trace(platform_key, key, _data, &block)
start = ::Process.clock_gettime ::Process::CLOCK_MONOTONIC
result = block.call
trvání = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) - start
observe(platform_key, key, duration)
result
end
def platform_field_key(type, field)
"graphql.#{typ.graphql_name}.#{pole.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: #{(trvání * 1000).round(5)} ms".yellow
konec
konec
Instalace je také velmi jednoduchá, stačí, když do schématu zahrnete informace o trasovacím zařízení. tracer (MyCustomTracer.new) konfigurace. Jako v příkladu níže:
# 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
Výstup z takového trasování vypadá takto:
Spuštěn POST "/graphql" pro ::1 v 2021-06-17 22:02:44 +0200
(0.1ms) SELECT sqlite_version(*)
Zpracování pomocí GraphqlController#execute jako */*
Parametry: {"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
Zatížení odkazů (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
Dokončeno 200 OK za 48 ms (Zobrazení: 1,2 ms | ActiveRecord: 2,0 ms | Alokace: 40128)
Souhrn
GraphQL již není novou technologií, ale řešení jejích problémů není plně standardizováno, pokud není součástí knihovny. Implementace této technologie v projekt poskytuje mnoho možností interakce s frontendem a osobně ji považuji za novou kvalitu ve vztahu k tomu, co nabízí REST API.