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
2019-03-08
Software Development

Optymalizacja kodu za pomocą Query Objects

The Codest

Tomasz Szkaradek

Architekt rozwoju

Całkiem prawdopodobne, że w pracy nie raz spotkałeś się z przeładowanymi modelami i ogromną liczbą wywołań w kontrolerach. Bazując na wiedzy zdobytej w środowisku Rails, w tym artykule zamierzam zaproponować proste rozwiązanie tego problemu.

Bardzo ważnym aspektem aplikacji railsowej jest zminimalizowanie ilości zbędnych zależności, dlatego też całe środowisko Railsowe w ostatnim czasie promuje podejście service object i wykorzystanie metody PORO (Pure Old Ruby Object). Opis tego, jak korzystać z takiego rozwiązania można znaleźć na stronie tutaj. W tym artykule rozwiążemy tę koncepcję krok po kroku i dostosujemy ją do problemu.

Problem

W hipotetycznej aplikacji mamy do czynienia ze skomplikowanym systemem transakcyjnym. Nasz model, reprezentujący każdą transakcję, ma zestaw zakresów, które pomagają uzyskać dane. Jest to duże ułatwienie pracy, ponieważ można je znaleźć w jednym miejscu. Nie trwa to jednak długo. Wraz z rozwojem aplikacji, w projekt staje się coraz bardziej skomplikowana. Zakresy nie mają już prostych odniesień "gdzie", brakuje nam danych i zaczynamy ładować relacje. Po pewnym czasie przypomina to skomplikowany system luster. A co gorsza, nie wiemy jak zrobić wieloliniową lambdę!

Poniżej znajduje się już rozszerzony model aplikacji. Przechowywane są w nim transakcje systemu płatności. Jak widać na poniższym przykładzie:

class Transaction  { where(visible: true) }

  scope(:active, lambda do
    joins(<<-SQL
      LEFT OUTER JOIN source ON transactions.source_id = source.id
      AND source.accepted_at IS NOT NULL
    SQL
  end)
end

Model to jedno, ale wraz ze wzrostem skali naszego projektu, kontrolery również zaczynają puchnąć. Spójrzmy na poniższy przykład:

class TransactionsController < ApplicationController
  def index
    @transactions = Transaction.for_publishers
                                   .active
                                   .visible
                                   .joins("LEFT JOIN withdrawal_items ON withdrawal_items.transaction_id = transactions.id")
                                   .joins("LEFT JOIN withdrawals ON withdrawals.id = withdrawal_items.withdrawal_id OR
 (withdrawals.id = source.resource_id AND source.resource_type = 'Withdrawal')")
                                   .order(:created_at)
                                   .page(params[:page])
                                   .per(params[:page])
    @transactions = apply_filters(@transactions)
  end
end

Widzimy tutaj wiele linii metod łańcuchowych wraz z dodatkowymi złączeniami, których nie chcemy wykonywać w wielu miejscach, a jedynie w tym konkretnym. Dołączone dane są później wykorzystywane przez metodę apply_filters, która dodaje odpowiednie filtrowanie danych na podstawie parametrów GET. Oczywiście możemy przenieść część z tych referencji do scope, ale czy nie jest to problem, który tak naprawdę próbujemy rozwiązać?

Rozwiązanie

Skoro znamy już problem, musimy go rozwiązać. W oparciu o odniesienie we wstępie, użyjemy tutaj podejścia PORO. W tym konkretnym przypadku podejście to nazywane jest obiektem zapytań, który jest rozwinięciem koncepcji obiektów usługowych.

Utwórzmy nowy katalog o nazwie "services", znajdujący się w katalogu apps naszego projektu. Tam utworzymy klasę o nazwie TransactionsQuery.

class TransactionsQuery
koniec

W kolejnym kroku musimy utworzyć inicjalizator, w którym zostanie utworzona domyślna ścieżka wywołania dla naszego obiektu

class TransactionsQuery
  def initialize(scope = Transaction.all)
    @scope = scope
  end
end

Dzięki temu będziemy mogli przenieść relację z aktywnego rekordu do naszego obiektu. Teraz możemy przenieść wszystkie nasze zakresy do klasy, które są potrzebne tylko w prezentowanym kontrolerze.

class TransactionsQuery
  def initialize(scope = Transaction.all)
    @scope = scope
  end

  private

  def active(scope)
    scope.joins(<<-SQL
      LEFT OUTER JOIN source ON transactions.source_id = source.id
      AND source.accepted_at IS NOT NULL
    SQL
  end

  def visible(scope)
    scope.where(visible: true)
  end

  def for_publishers(scope)
    scope.select("transactions.*")
         .joins(:account)
         .where("accounts.owner_type = 'Publisher'")
         .joins("JOIN publishers ON owner_id = publishers.id")
  end
end

Wciąż brakuje nam najważniejszej części, czyli zebrania danych w jeden ciąg i upublicznienia interfejsu. Metoda, w której połączymy wszystko razem, zostanie nazwana "call".

Co naprawdę ważne, użyjemy tam zmiennej instancji @scope, gdzie znajduje się zakres naszego wywołania.

class TransactionsQuery
  ...
  def call
    visible(@scope)
        .then(&method(:active))
        .then(&method(:for_publishers))
        .order(:created_at)
  end

  private
  ...
end

Cała klasa prezentuje się następująco:

class TransactionsQuery
  def initialize(scope = Transaction.all)
    @scope = scope
  end

  def call
    visible(@scope)
        .then(&method(:active))
        .then(&method(:for_publishers))
        .order(:created_at)
  end

  private

  def active(scope)
    scope.joins(<<-SQL
      LEFT OUTER JOIN source ON transactions.source_id = source.id
      AND source.accepted_at IS NOT NULL
    SQL
  end

  def visible(scope)
    scope.where(visible: true)
  end

  def for_publishers(scope)
    scope.select("transactions.*")
         .joins(:account)
         .where("accounts.owner_type = 'Publisher'")
         .joins("JOIN publishers ON owner_id = publishers.id")
  end
end

Po naszym oczyszczeniu model wygląda zdecydowanie lżej. Tam skupiamy się tylko na walidacji danych i relacjach między innymi modelami.

class Transaction < ActiveRecord::Base
  belongs_to :account
  has_one :withdrawal_item
end

Kontroler zaimplementował już nasze rozwiązanie; przenieśliśmy wszystkie dodatkowe zapytania do osobnej klasy. Jednak wywołania, których nie mieliśmy w modelu, pozostają nierozwiązaną kwestią. Po kilku zmianach, nasza akcja indeksu wygląda następująco:

class TransactionsController < ApplicationController
  def index
    @transactions = TransactionsQuery.new
                                     .call
                                     .joins("LEFT JOIN withdrawal_items ON withdrawal_items.accounting_event_id = transactions.id")
                                     .joins("LEFT JOIN withdrawals ON withdrawals.id = withdrawal_items.withdrawal_id OR
 (withdrawals.id = source.resource_id AND source.resource_type = 'Withdrawal')")
                                     .order(:created_at)
                                     .page(params[:page])
                                     .per(params[:page])
    @transactions = apply_filters(@transactions)
  end
end

Rozwiązanie

W przypadku wdrażania dobrych praktyk i konwencji, dobrym pomysłem może być zastąpienie wszystkich podobnych wystąpień danego problemu. Dlatego przeniesiemy zapytanie SQL z akcji indeksu do oddzielnego obiektu zapytania. Nazwiemy to TransactionsFilterableQuery zajęcia. Styl, w jakim przygotowujemy zajęcia, będzie podobny do tego prezentowanego w TransactionsQuery. W ramach kod zmiany, bardziej intuicyjny zapis dużych zapytań SQL będzie przemycany za pomocą wielowierszowych ciągów znaków o nazwie heredoc. Dostępne rozwiązanie znajdziesz poniżej:

class TransactionsFilterableQuery
  def initialize(scope = Transaction.all)
    @scope = scope
  end

  def call
    withdrawal(@scope).then(&method(:withdrawal_items))
  end

  private

  def withdrawal(scope)
    scope.joins(<<-SQL
      LEFT JOIN withdrawals ON withdrawals.id = withdrawal_items.withdrawal_id OR
      (withdrawals.id = source.resource_id AND source.resource_type = 'Withdrawal')
    SQL
  end

  def withdrawal_items(scope)
    scope.joins(<<-SQL
      LEFT JOIN withdrawal_items ON withdrawal_items.accounting_event_id = transactions.id
    SQL
  end
end

W przypadku zmian w kontrolerze redukujemy masę wierszy poprzez dodanie obiektu zapytania. Ważne jest, aby oddzielić wszystko oprócz części odpowiedzialnej za paginację.

class TransactionsController < ApplicationController
  def index
    @transactions = TransactionsQuery.new.call.then do |scope|
      TransactionsFilterableQuery.new(scope).call
    end.page(params[:page]).per(params[:page])

    @transactions = apply_filters(@transactions)
  end
end

Podsumowanie

Obiekt zapytania zmienia wiele w podejściu do pisania zapytań SQL. W ActiveRecord bardzo łatwo jest umieścić całą logikę biznesową i bazodanową w modelu, ponieważ wszystko jest w jednym miejscu. Sprawdzi się to całkiem dobrze w przypadku mniejszych aplikacji. Wraz ze wzrostem złożoności projektu, logikę umieszczamy w innych miejscach. Ten sam obiekt zapytania pozwala na grupowanie zapytań członkowskich do określonego problemu.

Dzięki temu mamy łatwą możliwość późniejszego dziedziczenia kodu, a ze względu na duck typing można te rozwiązania wykorzystywać również w innych modelach. Wadą tego rozwiązania jest większa ilość kodu i fragmentacja odpowiedzialności. Jednak to czy chcemy podjąć takie wyzwanie czy nie, zależy od nas i tego jak bardzo przeszkadzają nam grube modele.

Powiązane artykuły

Abstrakcyjna ilustracja malejącego wykresu słupkowego z rosnącą strzałką i złotą monetą symbolizującą efektywność kosztową lub oszczędności. Logo The Codest pojawia się w lewym górnym rogu wraz ze sloganem "In Code We Trust" na jasnoszarym tle.
Software Development

Jak skalować zespół programistów bez utraty jakości produktu?

Skalujesz swój zespół programistów? Dowiedz się, jak się rozwijać bez poświęcania jakości produktu. W tym przewodniku omówiono oznaki, że nadszedł czas na skalowanie, strukturę zespołu, zatrudnianie, przywództwo i narzędzia - a także sposób, w jaki The Codest może...

THEECODEST
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

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