Η GraphQL, όπως κάθε τεχνολογία, έχει τα προβλήματά της, ορισμένα από αυτά προκύπτουν άμεσα από την αρχιτεκτονική και ορισμένα είναι πανομοιότυπα με αυτά που βλέπουμε σε οποιαδήποτε άλλη εφαρμογή. Ωστόσο, οι λύσεις είναι εντελώς διαφορετικές.
Για να παρουσιάσουμε το πρόβλημα, ας υποθέσουμε την ακόλουθη αρχιτεκτονική εφαρμογής:
Και εδώ το αντίστοιχο ερώτημα στο GraphQL για να κατεβάσετε τα δεδομένα. Λαμβάνουμε όλους τους συνδέσμους, μαζί με την αφίσα και τους συνδέσμους της που προστίθενται στο σύστημα,
{
allLinks {
id
url
description
createdAt
postedBy {
id
name
links {
id
}
}
}
}
Όπως φαίνεται παρακάτω, μπορούμε να δούμε το κλασικό πρόβλημα n+1 με τις σχέσεις εδώ.
Φόρτωση συνδέσμων (0.4ms) SELECT "links".* FROM "links" ORDER BY created_at DESC
↳ app/controllers/graphql_controller.rb:5:in `execute'
Φόρτωση χρηστών (0.3ms) SELECT "users".* FROM "users" WHERE "users". "id" = ? LIMIT ? [["id", 40], ["LIMIT", 1]]
↳ app/controllers/graphql_controller.rb:5:in `execute'
Φόρτωση συνδέσμων (0.3ms) SELECT "links".* FROM "links" WHERE "links". "user_id" = ? [["user_id", 40]]
↳ app/controllers/graphql_controller.rb:5:in `execute'
Φόρτωση χρηστών (0.1ms) SELECT "users".* FROM "users" WHERE "users". "id" = ? LIMIT ? [["id", 38], ["LIMIT", 1]]
↳ app/controllers/graphql_controller.rb:5:in `execute'
Φόρτωση συνδέσμων (0.1ms) SELECT "links".* FROM "links" WHERE "links". "user_id" = ? [["user_id", 38]]
↳ app/controllers/graphql_controller.rb:5:in `execute'
Φόρτωση χρήστη (0.2ms) SELECT "users".* FROM "users" WHERE "users". "id" = ? LIMIT ? [["id", 36], ["LIMIT", 1]]
↳ app/controllers/graphql_controller.rb:5:in `execute'
Φόρτωση συνδέσμων (0.1ms) SELECT "links".* FROM "links" WHERE "links". "user_id" = ? [["user_id", 36]]
↳ app/controllers/graphql_controller.rb:5:in `execute'
Φόρτωση χρήστη (0.1ms) SELECT "users".* FROM "users" WHERE "users". "id" = ? LIMIT ? [["id", 34], ["LIMIT", 1]]
↳ app/controllers/graphql_controller.rb:5:in `execute'
Φόρτωση συνδέσμων (0.2ms) SELECT "links".* FROM "links" WHERE "links". "user_id" = ? [["user_id", 34]]
↳ app/controllers/graphql_controller.rb:5:in `execute'
Φόρτωση χρήστη (0.1ms) SELECT "users".* FROM "users" WHERE "users". "id" = ? LIMIT ? [["id", 32], ["LIMIT", 1]]
Σε αυτή την περίπτωση, λειτουργεί ακριβώς όπως αυτό το κομμάτι του κωδικός: Link.all.map(&:user).map(&:links).
Φαίνεται ότι γνωρίζουμε τη λύση του προβλήματος: Link.includes(user: :links).map(&:user).map(&:links), αλλά θα λειτουργήσει πραγματικά; Ας το ελέγξουμε!
Για να επαληθεύσω τη διόρθωση, άλλαξα το GraphQL ερώτημα για να χρησιμοποιήσετε μόνο μερικά πεδία και καμία σχέση.
{
allLinks {
id
url
description
createdAt
}
}
Δυστυχώς, το αποτέλεσμα δείχνει ότι, παρά την έλλειψη συνδέσμων σε σχέση με τον χρήστη και τους συνδέσμους του, εξακολουθούμε να επισυνάπτουμε αυτά τα δεδομένα στο ερώτημα της βάσης δεδομένων. Δυστυχώς, είναι περιττά και, με μια ακόμη πιο περίπλοκη δομή, αποδεικνύεται απλώς αναποτελεσματικό.
Στο GraphQL, τέτοια προβλήματα επιλύονται διαφορετικά,απλά με τη φόρτωση δεδομένων σε παρτίδες, υποθέτοντας ότι τα δεδομένα είναι απαραίτητα όταν μπαίνουν στο ερώτημα. Πρόκειται για μια τέτοια τεμπέλικη φόρτωση. Μια από τις πιο δημοφιλείς βιβλιοθήκες είναι η https://github.com/Shopify/graphql-batch/.
Δυστυχώς, η εγκατάστασή του δεν είναι τόσο εύκολη όσο φαίνεται. Οι φορτωτές δεδομένων είναι διαθέσιμοι εδώ: https://github.com/Shopify/graphql-batch/tree/master/examples, εννοώ το RecordLoader και η κλάσηAssociationLoader κατηγορία. Ας εγκαταστήσουμε κλασικά την gem 'graphql-batch' βιβλιοθήκη και στη συνέχεια να την προσθέσουμε στο σχήμα μας, καθώς και στους φορτωτές:
# graphql-ruby/app/graphql/graphql_tutorial_schema.rb
class GraphqlTutorialSchema < GraphQL::Schema
query Types::QueryType
mutation Types::MutationType
use GraphQL::Batch
...
end
Και οι τύποι μας:
# graphql-ruby/app/graphql/types/link_type.rb
module Τύποι
class LinkType < BaseNode
πεδίο :created_at, DateTimeType, null: false
πεδίο :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
module Types
class UserType < BaseNode
field :created_at, DateTimeType, null: false
πεδίο :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
Ως αποτέλεσμα της χρήσης των φορτωτών, ομαδοποιούμε τα δεδομένα και αναζητούμε δεδομένα σε δύο απλά ερωτήματα sql:
Τα ερωτήματα N + 1 δεν είναι τα πάντα, στο GraphQL μπορούμε να μεταφέρουμε ελεύθερα τα επόμενα χαρακτηριστικά. Από προεπιλογή, έχει οριστεί σε 1. Αυτό μπορεί μερικές φορές να είναι υπερβολικό για τον διακομιστή, ειδικά σε μια κατάσταση όπου μπορούμε να φωλιάζουμε ελεύθερα δεδομένα. Πώς να το αντιμετωπίσετε; Μπορούμε να περιορίσουμε την πολυπλοκότητα του ερωτήματος, αλλά για να το κάνουμε αυτό, πρέπει επίσης να καθορίσουμε το κόστος τους στα χαρακτηριστικά. Από προεπιλογή έχει οριστεί σε 1. Ορίζουμε αυτό το κόστος χρησιμοποιώντας την εντολή πολυπλοκότητα: χαρακτηριστικό, όπου μπορούμε να εισάγουμε δεδομένα: field: links, [LinkType], null: false, complexity: 101. Αν ο περιορισμός πρόκειται να λειτουργήσει πραγματικά, θα πρέπει ακόμα να εισαγάγετε το μέγιστο όριο στο σύστημά σας:
class GraphqlTutorialSchema < GraphQL::Schema
query Types::QueryType
mutation Types::MutationType
use GraphQL::Batch
max_complexity 100
...
end
Εντοπισμός
GraphQL επεξεργάζεται τα ερωτήματα με διαφορετικό τρόπο και η ανίχνευση δεν είναι τόσο απλή αν συγκριθεί με αυτό που μπορούμε να κάνουμε τοπικά. Δυστυχώς, το rack mini profiler ή ένα κανονικό αρχείο καταγραφής SQL δεν θα μας πει τα πάντα και δεν θα μας υποδείξει ποιο μέρος του ερωτήματος είναι υπεύθυνο για μια συγκεκριμένη χρονική φέτα. Στην περίπτωση της GraphQL-Ruby, μπορούμε να χρησιμοποιήσουμε εμπορικές λύσεις που είναι διαθέσιμες εδώ: https://graphql-ruby.org/queries/tracing, ή να προσπαθήσουμε να προετοιμάσουμε τη δική μας ανίχνευση. Παρακάτω, το απόσπασμα μοιάζει με έναν τοπικό ανιχνευτή.
# 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
duration = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) - start
observe(platform_key, key, duration)
αποτέλεσμα
τέλος
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
Η εγκατάσταση είναι επίσης εξαιρετικά απλή, θα πρέπει να συμπεριλάβετε τις πληροφορίες του ανιχνευτή στο σχήμα tracer (MyCustomTracer.new) διαμόρφωση. Όπως στο παρακάτω παράδειγμα:
# 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
Η έξοδος από μια τέτοια ανίχνευση μοιάζει ως εξής:
Ξεκίνησε POST "/graphql" για ::1 στις 2021-06-17 22:02:44 +0200
(0.1ms) SELECT sqlite_version(*)
Επεξεργασία από GraphqlController#execute ως */*
Παράμετροι: {"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}"}}
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
Φόρτωση συνδέσμων (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
Ολοκληρώθηκε 200 OK σε 48ms (Views: 1.2ms | ActiveRecord: 2.0ms | Allocations: 40128)
Περίληψη
GraphQL δεν είναι πλέον μια νέα τεχνολογία, αλλά οι λύσεις στα προβλήματά της δεν είναι πλήρως τυποποιημένες αν δεν αποτελούν μέρος της βιβλιοθήκης. Η εφαρμογή αυτής της τεχνολογίας στην έργο δίνει πολλές δυνατότητες αλληλεπίδρασης με το frontend και προσωπικά θεωρώ ότι αποτελεί μια νέα ποιότητα σε σχέση με αυτό που προσφέρει το REST API.