window.pipedriveLeadboosterConfig = { base: 'leadbooster-chat.pipedrive.com', companyId: 11580370, playbookUuid: '22236db1-6d50-40c4-b48f-8b11262155be', version: 2, } ;(function () { var w = window if (w.LeadBooster) { console.warn('LeadBooster już istnieje') } else { w.LeadBooster = { q: [], on: function (n, h) { this.q.push({ t: 'o', n: n, h: h }) }, trigger: function (n) { this.q.push({ t: 't', n: n }) }, } } })() GraphQL Ruby. Co z wydajnością? - The Codest
The Codest
  • O nas
  • Nasze Usługi
    • Software Development
      • Frontend Development
      • Backend Development
    • Zespoły IT
      • Programiści frontendowi
      • Backend Dev
      • Inżynierowie danych
      • Inżynierowie rozwiązań chmurowych
      • Inżynierowie QA
      • Inne
    • Konsultacje IT
      • Audyt i doradztwo
  • Branże
    • Fintech i bankowość
    • E-commerce
    • Adtech
    • Healthtech
    • Produkcja
    • Logistyka
    • Motoryzacja
    • IOT
  • Wartość dla
    • CEO
    • CTO
    • Delivery Managera
  • Nasz zespół
  • Case Studies
  • Nasze Know How
    • Blog
    • Meetups
    • Webinary
    • Raporty
Kariera Skontaktuj się z nami
  • O nas
  • Nasze Usługi
    • Software Development
      • Frontend Development
      • Backend Development
    • Zespoły IT
      • Programiści frontendowi
      • Backend Dev
      • Inżynierowie danych
      • Inżynierowie rozwiązań chmurowych
      • Inżynierowie QA
      • Inne
    • Konsultacje IT
      • Audyt i doradztwo
  • Wartość dla
    • CEO
    • CTO
    • Delivery Managera
  • Nasz zespół
  • Case Studies
  • Nasze Know How
    • Blog
    • Meetups
    • Webinary
    • Raporty
Kariera Skontaktuj się z nami
Strzałka w tył WSTECZ
2021-06-30
Software Development

GraphQL Ruby. Co z wydajnością?

The Codest

Tomasz Szkaradek

Architekt rozwoju

GraphQL, jak każda technologia, ma swoje problemy, niektóre z nich wynikają bezpośrednio z architektury, a niektóre są identyczne z tym, co widzimy w każdej innej aplikacji. Rozwiązania są jednak zupełnie inne.

Aby przedstawić problem, załóżmy następującą architekturę aplikacji:

https://drive.google.com/file/d/1N4sWPJSls0S8FFHbpHCUVHBNBpEuSsyz/view

A tutaj odpowiadające zapytanie w GraphQL aby pobrać dane. Pobieramy wszystkie linki, wraz z plakatem i jego linkami dodanymi do systemu,

{
  allLinks {
    id
    url
    opis
    createdAt
    postedBy {
      id
      nazwa
      links {
        id
      }
    }
  }
}

Jak pokazano poniżej, widzimy tutaj klasyczny problem n + 1 z relacjami.

Link Load (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'
  Link Load (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'
  Link Load (0.1ms) SELECT "links".* FROM "links" WHERE "links". "user_id" = ?  [["user_id", 38]]
  ↳ app/controllers/graphql_controller.rb:5:in `execute'
  User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users". "id" = ? LIMIT ?  [["id", 36], ["LIMIT", 1]]
  ↳ app/controllers/graphql_controller.rb:5:in `execute'
  Link Load (0.1ms) SELECT "links".* FROM "links" WHERE "links". "user_id" = ?  [["user_id", 36]]
  ↳ app/controllers/graphql_controller.rb:5:in `execute'
  User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users". "id" = ? LIMIT ?  [["id", 34], ["LIMIT", 1]]
  ↳ app/controllers/graphql_controller.rb:5:in `execute'
  Link Load (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]]

W tym przypadku działa to dokładnie tak, jak ten kawałek kod:
Link.all.map(&:user).map(&:links).

Wydaje się, że znamy rozwiązanie tego problemu: Link.includes(user: :links).map(&:user).map(&:links)ale czy to naprawdę działa? Sprawdźmy to!

Aby zweryfikować poprawkę, zmieniłem GraphQL zapytanie, aby użyć tylko kilku pól i żadnych relacji.

{
  allLinks {
    id
    url
    opis
    createdAt
  }
}

Niestety wynik pokazuje, że pomimo braku powiązań w stosunku do użytkownika i jego linków, nadal dołączamy te dane do zapytania bazodanowego. Niestety są one nadmiarowe, a przy jeszcze bardziej skomplikowanej strukturze okazuje się to po prostu nieefektywne.

Przetwarzanie przez GraphqlController#execute jako */*
  Parametry: {"query"=>"{n allLinks {n idn urln descriptionn createdAtn }n}", "graphql"=>{"query"=>"{n allLinks {n idn urln descriptionn createdAtn }n}"}}.
  Link Load (0.3ms) SELECT "links".* FROM "links" ORDER BY created_at DESC
  app/controllers/graphql_controller.rb:5:in `execute'
  Ładowanie użytkowników (0.3ms) SELECT "users".* FROM "users" WHERE "users". "id" IN (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) [["id", 40], ["id", 38], ["id", 36], ["id", 34], ["id", 32], ["id", 30], ["id", 28], ["id", 26], ["id", 24], ["id", 22], ["id", 20], ["id", 18], ["id", 16], ["id", 14], ["id", 12], ["id", 10], ["id", 8], ["id", 6], ["id", 4], ["id", 2]].
  ↳ app/controllers/graphql_controller.rb:5:in `execute'
  Link Load (0.3ms) SELECT "links".* FROM "links" WHERE "links". "user_id" IN (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) [["user_id"].) [["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/controllers/graphql_controller.rb:5:in `execute'
Ukończono 200 OK w 39 ms (Views: 0,7 ms | ActiveRecord: 0,9 ms | Allocations: 8730)

W GraphQLTakie problemy rozwiązuje się inaczej, po prostu ładując dane partiami, zakładając, że dane są potrzebne w momencie umieszczania ich w zapytaniu. Jest to takie leniwe ładowanie. Jedną z najpopularniejszych bibliotek jest https://github.com/Shopify/graphql-batch/.

Niestety jego instalacja nie jest tak bezproblemowa, jak mogłoby się wydawać. Programy ładujące dane są dostępne tutaj: https://github.com/Shopify/graphql-batch/tree/master/examples, mam na myśli plik RecordLoader klasa iAssociationLoader class. Klasycznie zainstalujmy klasę gem 'graphql-batch' a następnie dodać ją do naszego schematu, a także programów ładujących:

# graphql-ruby/app/graphql/graphql_tutorial_schema.rb
class GraphqlTutorialSchema < GraphQL::Schema
  query Types::QueryType
  mutation Types::MutationType
  use GraphQL::Batch
  ...
end

I nasze typy:

# graphql-ruby/app/graphql/types/link_type.rb
moduł Typy
  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
moduł Types
  class UserType < BaseNode
    field :created_at, DateTimeType, null: false
    field :name, String, null: false
    field :email, String, null: false
    field :votes, [VoteType], null: false
    pole :links, [LinkType], null: false

    def links
      Loaders::AssociationLoader.for(User, :links).load(object)
    end
  end
end

W wyniku korzystania z loaderów, dane są wsadowe, a zapytania o dane są składane w dwóch prostych zapytaniach sql:

Rozpoczęto POST "/graphql" dla ::1 o 2021-06-16 22:40:17 +0200
   (0.1ms) SELECT sqlite_version(*)
Przetwarzanie przez GraphqlController#execute jako */*
  Parametry: {"query"=>"{n allLinks {n idn urln descriptionn createdAtn postedBy {n idn namen links {n idn }n }}", "graphql"=>{"query"=>"{n allLinks {n idn urln descriptionn createdAtn postedBy {n idn namen links {n idn }n }}"}}.
  Link Load (0.4ms) SELECT "links".* FROM "links"
  app/controllers/graphql_controller.rb:5:in `execute'
  Ładowanie użytkowników (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'
  Link Load (0.5ms) 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'
Ukończono 200 OK w 62 ms (widoki: 1,3 ms | ActiveRecord: 1,8 ms | alokacje: 39887)

Istnieją również inne rozwiązania, które rozwiązują ten problem, np:

https://github.com/exAspArk/batch-loader#basic-example

Złożoność zapytań

N + 1 zapytań to nie wszystko, w GraphQL możemy swobodnie przenosić kolejne atrybuty. Domyślnie jest on ustawiony na 1. Może to być czasem zbyt dużo dla serwera, szczególnie w sytuacji, gdy możemy dowolnie zagnieżdżać dane. Jak sobie z tym poradzić? Możemy ograniczyć złożoność zapytań, ale w tym celu musimy również określić ich koszt w atrybutach. Domyślnie jest on ustawiony na 1. Koszt ten ustawiamy za pomocą atrybutu złożoność: atrybut, w którym możemy wprowadzić dane: field: links, [LinkType], null: false, complexity: 101. Jeśli ograniczenie ma faktycznie działać, nadal musisz wprowadzić maksymalny limit do swojego programu:

class GraphqlTutorialSchema < GraphQL::Schema
  query Types::QueryType
  mutation Types::MutationType
  use GraphQL::Batch
  max_complexity 100
  ...
end

Śledzenie

GraphQL SQL przetwarza zapytania w różny sposób, a śledzenie nie jest takie proste w porównaniu do tego, co możemy zrobić lokalnie. Niestety rack mini profiler czy zwykły log SQL nie powie nam wszystkiego i nie wskaże, która część zapytania odpowiada za dany wycinek czasu. W przypadku GraphQL-Ruby możemy skorzystać z komercyjnych rozwiązań dostępnych tutaj: https://graphql-ruby.org/queries/tracinglub spróbować przygotować własny tracing. Poniżej, snippet wygląda jak lokalny 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'
  }

  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)
    result
  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

Instalacja jest również niezwykle prosta, należy dołączyć informacje o znaczniku do schematu tracer (MyCustomTracer.new) konfiguracja. Jak w poniższym przykładzie:

# 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

Dane wyjściowe z takiego śledzenia wyglądają następująco:

Rozpoczęto POST "/graphql" dla ::1 o 2021-06-17 22:02:44 +0200
   (0.1ms) SELECT sqlite_version(*)
Przetwarzanie przez GraphqlController#execute jako */*
  Parametry: {"query"=>"{n allLinks {n idn urln descriptionn createdAtn postedBy {n idn namen links {n idn }n }}", "graphql"=>{"query"=>"{n allLinks {n idn urln descriptionn createdAtn postedBy {n idn namen links {n idn }n }}"}}.
platform_key: graphql.lex, key: lex, duration: 0.156 ms
platform_key: graphql.parse, klucz: parse, czas trwania: 0.108 ms
platform_key: graphql.validate, klucz: validate, czas trwania: 0.537 ms
platform_key: graphql.analyze_query, klucz: analyze_query, czas trwania: 0.123 ms
platform_key: graphql.analyze_multiplex, klucz: analyze_multiplex, czas trwania: 0.159 ms
  Obciążenie łącza (0,4 ms) SELECT "links".* FROM "links"
  ↳ app/graphql/graphql_tutorial_schema.rb:21:in `platform_trace'
platform_key: graphql.execute_query, klucz: execute_query, czas trwania: 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, klucz: execute_query_lazy, czas trwania: 14.12 ms
platform_key: graphql.execute_multiplex, klucz: execute_multiplex, czas trwania: 31.11 ms
Ukończono 200 OK w 48 ms (Views: 1.2 ms | ActiveRecord: 2.0 ms | Allocations: 40128)

Podsumowanie

GraphQL nie jest już nową technologią, ale rozwiązania jej problemów nie są w pełni ustandaryzowane, jeśli nie są częścią biblioteki. Wdrożenie tej technologii w projekt daje wiele możliwości interakcji z frontendem i osobiście uważam to za nową jakość w stosunku do tego, co oferuje REST API.

Dlaczego (prawdopodobnie) powinieneś używać Typescript

Jak nie zabić projektu złymi praktykami kodowania?

Strategie pobierania danych w NextJS

Powiązane artykuły

Software Development

Tworzenie przyszłościowych aplikacji internetowych: spostrzeżenia zespołu ekspertów The Codest

Odkryj, w jaki sposób The Codest wyróżnia się w tworzeniu skalowalnych, interaktywnych aplikacji internetowych przy użyciu najnowocześniejszych technologii, zapewniając płynne doświadczenia użytkowników na wszystkich platformach. Dowiedz się, w jaki sposób nasza wiedza napędza transformację cyfrową i biznes...

THEECODEST
Software Development

10 najlepszych firm tworzących oprogramowanie na Łotwie

Dowiedz się więcej o najlepszych łotewskich firmach programistycznych i ich innowacyjnych rozwiązaniach w naszym najnowszym artykule. Odkryj, w jaki sposób ci liderzy technologiczni mogą pomóc w rozwoju Twojej firmy.

thecodest
Rozwiązania dla przedsiębiorstw i scaleupów

Podstawy tworzenia oprogramowania Java: Przewodnik po skutecznym outsourcingu

Zapoznaj się z tym niezbędnym przewodnikiem na temat skutecznego tworzenia oprogramowania Java outsourcing, aby zwiększyć wydajność, uzyskać dostęp do wiedzy specjalistycznej i osiągnąć sukces projektu z The Codest.

thecodest
Software Development

Kompletny przewodnik po outsourcingu w Polsce

Wzrost liczby outsourcing w Polsce jest napędzany przez postęp gospodarczy, edukacyjny i technologiczny, sprzyjający rozwojowi IT i przyjazny klimat dla biznesu.

TheCodest
Rozwiązania dla przedsiębiorstw i scaleupów

Kompletny przewodnik po narzędziach i technikach audytu IT

Audyty IT zapewniają bezpieczne, wydajne i zgodne z przepisami systemy. Dowiedz się więcej o ich znaczeniu, czytając cały artykuł.

The Codest
Jakub Jakubowicz CTO & Współzałożyciel

Subskrybuj naszą bazę wiedzy i bądź na bieżąco!

    O nas

    The Codest - Międzynarodowa firma programistyczna z centrami technologicznymi w Polsce.

    Wielka Brytania - siedziba główna

    • Office 303B, 182-184 High Street North E6 2JA
      Londyn, Anglia

    Polska - lokalne centra technologiczne

    • Fabryczna Office Park, Aleja
      Pokoju 18, 31-564 Kraków
    • Brain Embassy, Konstruktorska
      11, 02-673 Warszawa, Polska

      The Codest

    • Strona główna
    • O nas
    • Nasze Usługi
    • Case Studies
    • Nasze Know How
    • Kariera
    • Słownik

      Nasze Usługi

    • Konsultacje IT
    • Software Development
    • Backend Development
    • Frontend Development
    • Zespoły IT
    • Backend Dev
    • Inżynierowie rozwiązań chmurowych
    • Inżynierowie danych
    • Inne
    • Inżynierowie QA

      Raporty

    • Fakty i mity na temat współpracy z zewnętrznym partnerem programistycznym
    • Z USA do Europy: Dlaczego amerykańskie startupy decydują się na relokację do Europy?
    • Porównanie centrów rozwoju Tech Offshore: Tech Offshore Europa (Polska), ASEAN (Filipiny), Eurazja (Turcja)
    • Jakie są największe wyzwania CTO i CIO?
    • The Codest
    • The Codest
    • The Codest
    • Privacy policy
    • Warunki korzystania z witryny

    Copyright © 2025 by The Codest. Wszelkie prawa zastrzeżone.

    pl_PLPolish
    en_USEnglish de_DEGerman sv_SESwedish da_DKDanish nb_NONorwegian fiFinnish fr_FRFrench arArabic it_ITItalian jaJapanese ko_KRKorean es_ESSpanish nl_NLDutch etEstonian elGreek pl_PLPolish