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 }) }, } } })() Optymalizacja kodu za pomocą Query Objects - 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
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

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