GraphQL, come qualsiasi tecnologia, ha i suoi problemi, alcuni dei quali derivano direttamente dall'architettura e altri sono identici a quelli che vediamo in qualsiasi altra applicazione. Tuttavia, le soluzioni sono completamente diverse.
Per presentare il problema, ipotizziamo la seguente architettura applicativa:
Ed ecco la corrispondente query in GraphQL per scaricare i dati. Recuperiamo tutti i link, insieme al poster e ai suoi link aggiunti al sistema,
{
tutti i link {
id
url
descrizione
creatoAt
postedBy {
id
nome
link {
id
}
}
}
}
Come mostrato di seguito, si può vedere il classico problema delle relazioni n + 1.
Carico dei link (0,4ms) SELECT "links".* FROM "links" ORDER BY created_at DESC
↳ app/controllers/graphql_controller.rb:5:in `execute'
Caricamento utenti (0,3ms) SELECT "users".* FROM "users" WHERE "users". "id" = ? LIMIT ? [["id", 40], ["LIMIT", 1]]
↳ app/controllers/graphql_controller.rb:5:in `execute'
Caricamento dei link (0,3ms) SELECT "links".* FROM "links" WHERE "links". "user_id" = ? [["user_id", 40]]
↳ app/controllers/graphql_controller.rb:5:in `execute'
Caricamento degli utenti (0,1ms) SELECT "users".* FROM "users" WHERE "users". "id" = ? LIMIT ? [["id", 38], ["LIMIT", 1]]
↳ app/controllers/graphql_controller.rb:5:in `execute'
Caricamento dei link (0,1 ms) SELECT "links".* FROM "links" WHERE "links". "user_id" = ? [["user_id", 38]]
↳ app/controllers/graphql_controller.rb:5:in `execute'
Caricamento degli utenti (0,2ms) SELECT "users".* FROM "users" WHERE "users". "id" = ? LIMIT ? [["id", 36], ["LIMIT", 1]]
↳ app/controllers/graphql_controller.rb:5:in `execute'
Caricamento dei link (0,1 ms) SELECT "links".* FROM "links" WHERE "links". "user_id" = ? [["user_id", 36]]
↳ app/controllers/graphql_controller.rb:5:in `execute'
Caricamento degli utenti (0,1ms) SELECT "users".* FROM "users" WHERE "users". "id" = ? LIMIT ? [["id", 34], ["LIMIT", 1]]
↳ app/controllers/graphql_controller.rb:5:in `execute'
Caricamento dei link (0,2ms) SELECT "links".* FROM "links" WHERE "links". "user_id" = ? [["user_id", 34]]
↳ app/controllers/graphql_controller.rb:5:in `execute'
Caricamento degli utenti (0,1ms) SELECT "users".* FROM "users" WHERE "users". "id" = ? LIMIT ? [["id", 32], ["LIMIT", 1]]
In questo caso, funziona esattamente come questo pezzo di codice: Link.all.map(&:utente).map(&:link).
Sembra che conosciamo la soluzione al problema: Link.includes(user: :links).map(&:user).map(&:links)ma funzionerà davvero? Scopriamolo!
Per verificare la correzione, ho modificato il file GraphQL per utilizzare solo alcuni campi e nessuna relazione.
{
tutti i link {
id
url
descrizione
createdAt
}
}
Sfortunatamente, il risultato mostra che, nonostante la mancanza di collegamenti in relazione all'utente e ai suoi link, si allegano comunque questi dati all'interrogazione del database. Purtroppo sono ridondanti e, con una struttura ancora più complicata, risultano semplicemente inefficienti.
In GraphQLTali problemi vengono risolti in modo diverso, semplicemente caricando i dati in batch, assumendo che i dati siano necessari quando vengono inseriti nella query. Si tratta di un caricamento pigro. Una delle librerie più diffuse è https://github.com/Shopify/graphql-batch/.
Purtroppo, la sua installazione non è così semplice come potrebbe sembrare. I caricatori di dati sono disponibili qui: https://github.com/Shopify/graphql-batch/tree/master/examples, intendo il file Caricatore di record e la classeCaricatore di associazioni classe. Installiamo classicamente la classe gemma 'graphql-batch' e poi aggiungerla al nostro schema e ai caricatori:
# graphql-ruby/app/graphql/graphql_tutorial_schema.rb
classe GraphqlTutorialSchema < GraphQL::Schema
query Tipi::QueryType
mutazione Types::MutationType
use GraphQL::Batch
...
fine
E i nostri tipi:
# graphql-ruby/app/graphql/types/link_type.rb
modulo Tipi
classe LinkType < BaseNode
campo :created_at, DateTimeType, null: false
campo :url, String, null: false
campo :description, Stringa, null: false
campo :posted_by, UserType, null: false, metodo: :user
campo :votes, [Types::VoteType], null: false
def utente
Loaders::RecordLoader.for(User).load(object.user_id)
fine
fine
fine
# graphql-ruby/app/graphql/types/user_type.rb
modulo Tipi
class UserType < BaseNode
campo :created_at, DateTimeType, null: false
campo :name, String, null: false
campo :email, Stringa, null: false
campo :votes, [VoteType], null: false
campo :link, [LinkType], null: false
def collegamenti
Loaders::AssociationLoader.for(User, :links).load(object)
fine
fine
fine
Grazie all'uso dei caricatori, i dati vengono raggruppati e interrogati con due semplici query sql:
Avviato POST "/graphql" per ::1 al 2021-06-16 22:40:17 +0200
(0.1ms) SELEZIONA sqlite_version(*)
Elaborazione da parte di GraphqlController#execute come */*
Parametri: {"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}"}}
Caricamento dei link (0.4ms) SELECT "links".* FROM "links"
↳ app/controllers/graphql_controller.rb:5:in `execute'
User Load (0.9ms) SELECT "users".* FROM "users" WHERE "users". "id" IN (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) [["id", 2], ["id", 4], ["id", 6], ["id", 8], ["id", 10], ["id", 12], ["id", 14], ["id", 16], ["id", 18], ["id", 20], ["id", 22], ["id", 24], ["id", 26], ["id", 28], ["id", 30], ["id", 32], ["id", 34], ["id", 36], ["id", 38], ["id", 40]]
↳ app/graphql/loaders/record_loader.rb:12:in `perform'
Caricamento dei link (0,5 ms) SELECT "links".* FROM "links" WHERE "links". "user_id" IN (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) [["user_id", 2], ["user_id", 4], ["user_id", 6], ["user_id", 8], ["user_id", 10], ["user_id", 12], ["user_id", 14], ["user_id", 16], ["user_id", 18], ["user_id", 20], ["user_id", 22], ["user_id", 24], ["user_id", 26], ["user_id", 28], ["user_id", 30], ["user_id", 32], ["user_id", 34], ["user_id", 36], ["user_id", 38], ["user_id", 40]]
↳ app/graphql/loaders/association_loader.rb:46:in `preload_association'
Completato 200 OK in 62 ms (Viste: 1,3 ms | ActiveRecord: 1,8 ms | Allocazioni: 39887)
Esistono anche altre soluzioni che risolvono questo problema, come ad esempio:
Complessità delle interrogazioni
N + 1 interrogazioni non sono tutto, in GraphQL possiamo trasportare liberamente gli attributi successivi. Per impostazione predefinita, è impostato a 1. Questo può essere talvolta eccessivo per il server, soprattutto in una situazione in cui possiamo annidare liberamente i dati. Come comportarsi? Possiamo limitare la complessità della query, ma per farlo dobbiamo anche specificare il loro costo negli attributi. Per impostazione predefinita è impostato a 1. Il costo può essere impostato utilizzando l'opzione complessità: dove si possono inserire i dati: campo: link, [LinkType], null: false, complessità: 101. Se si vuole che la limitazione funzioni davvero, è necessario introdurre il limite massimo nel proprio schema:
classe GraphqlTutorialSchema < GraphQL::Schema
query Tipi::QueryType
mutazione Types::MutationType
use GraphQL::Batch
max_complessità 100
...
fine
Tracciamento
GraphQL elabora le query in modo diverso e il tracciamento non è così semplice se confrontato con quello che possiamo fare localmente. Sfortunatamente, il mini profiler di rack o un normale log di SQL non ci diranno tutto e non indicheranno quale parte della query è responsabile di un determinato lasso di tempo. Nel caso di GraphQL-Ruby, possiamo utilizzare le soluzioni commerciali disponibili qui: https://graphql-ruby.org/queries/tracingo provare a preparare un proprio tracciante. Di seguito, lo snippet appare come un tracciante locale.
Anche l'installazione è estremamente semplice, è necessario includere le informazioni del tracciatore nello schema tracer (MyCustomTracer.new) configurazione. Come nell'esempio seguente:
# graphql-ruby/app/graphql/graphql_tutorial_schema.rb
classe GraphqlTutorialSchema < GraphQL::Schema
query Tipi::QueryType
mutazione Types::MutationType
use GraphQL::Batch
tracer(MyCustomTracer.new)
...
fine
L'output di questo tipo di tracciamento è simile a questo:
Avviato POST "/graphql" per ::1 al 2021-06-17 22:02:44 +0200
(0.1ms) SELEZIONA sqlite_version(*)
Elaborazione da parte di GraphqlController#execute come */*
Parametri: {"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, chiave: parse, durata: 0,108 ms
chiave_piattaforma: graphql.validate, chiave: validate, durata: 0,537 ms
platform_key: graphql.analyze_query, chiave: analyze_query, durata: 0,123 ms
chiave_piattaforma: graphql.analyze_multiplex, chiave: analyze_multiplex, durata: 0,159 ms
Carico dei link (0,4 ms) SELEZIONA "link".* DA "link"
↳ 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
chiave_piattaforma: graphql.execute_multiplex, chiave: execute_multiplex, durata: 31,11 ms
Completato 200 OK in 48 ms (Viste: 1,2 ms | ActiveRecord: 2,0 ms | Allocazioni: 40128)
Sintesi
GraphQL non è più una tecnologia nuova, ma le soluzioni ai suoi problemi non sono completamente standardizzate se non fanno parte della biblioteca. L'implementazione di questa tecnologia nella progetto offre molte opportunità di interazione con il frontend e personalmente la considero una nuova qualità rispetto a quanto offerto dalle API REST.