Det er meget sandsynligt, at du på dit arbejde er stødt på overbelastede modeller og et stort antal kald i controllerne en hel del gange. Baseret på viden om Rails-miljøet vil jeg i denne artikel foreslå en enkel løsning på dette problem.
Et meget vigtigt aspekt af Rails-applikationen er at minimere antallet af overflødige afhængigheder, hvilket er grunden til, at hele Rails-miljøet for nylig har fremmet serviceobjekttilgangen og brugen af PORO-metoden (Pure Old Ruby Object). En beskrivelse af, hvordan man bruger en sådan løsning, finder du her her. I denne artikel vil vi løse konceptet trin for trin og tilpasse det til problemet.
Problem
I en hypotetisk applikation har vi at gøre med et kompliceret transaktionssystem. Vores model, som repræsenterer hver transaktion, har et sæt scopes, som hjælper dig med at hente data. Det er en stor arbejdslettelse, da det kan findes ét sted. Men det varer ikke længe. Med udviklingen af applikationen bliver projekt bliver mere og mere kompliceret. Scopes har ikke længere simple 'where'-referencer, vi mangler data og begynder at indlæse relationer. Efter et stykke tid minder det om et kompliceret system af spejle. Og hvad værre er, vi ved ikke, hvordan man laver en lambda med flere linjer!
Nedenfor finder du en allerede udvidet applikationsmodel. Betalingssystemets transaktioner er gemt i. Som du kan se i eksemplet nedenfor:
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)
slut
Modellen er én ting, men når omfanget af vores projekt stiger, begynder controllerne også at svulme op. Lad os se på eksemplet nedenfor:
class TransactionsController < ApplikationsController
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)
slut
slut
Her kan vi se mange linjer med kædede metoder sammen med yderligere sammenføjninger, som vi ikke ønsker at udføre mange steder, men kun i netop denne. De vedhæftede data bruges senere af metoden apply_filters, som tilføjer den passende datafiltrering baseret på GET-parametrene. Vi kan selvfølgelig overføre nogle af disse referencer til scope, men er det ikke det problem, vi faktisk forsøger at løse?
Løsning
Da vi allerede ved, at vi har et problem, skal vi løse det. Baseret på referencen i indledningen vil vi bruge PORO-tilgangen her. I dette tilfælde kaldes denne tilgang for forespørgselsobjektet, som er en udvikling af serviceobjektkonceptet.
Lad os oprette en ny mappe med navnet "services", som ligger i vores projekts app-mappe. Der vil vi oprette en klasse ved navn TransactionsQuery
.
klasse TransactionsQuery
slutning
Som et næste skridt skal vi oprette en initializer, hvor der oprettes en standardopkaldssti til vores objekt
klasse TransactionsQuery
def initialize(scope = Transaction.all)
@scope = område
slut
end
Takket være dette vil vi være i stand til at overføre relationen fra den aktive post til vores facilitet. Nu kan vi overføre alle vores scopes til klassen, som kun er nødvendige i den præsenterede controller.
klasse TransactionsQuery
def initialize(scope = Transaction.all)
@scope = område
slut
privat
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("transaktioner.*")
.joins(:account)
.where("accounts.owner_type = 'Publisher'")
.joins("JOIN publishers ON owner_id = publishers.id")
slut
slut
Vi mangler stadig den vigtigste del, nemlig at samle data i en streng og gøre grænsefladen offentlig. Den metode, hvor vi samler det hele, får navnet "call".
Det, der virkelig er vigtigt, er, at vi bruger @scope-instansvariablen der, hvor omfanget af vores kald er placeret.
klasse TransactionsQuery
...
def opkald
visible(@scope)
.then(&method(:active))
.then(&method(:for_publishers))
.order(:created_at)
slut
privat
...
end
Hele klassen præsenterer sig som følger:
klasse TransactionsQuery
def initialize(scope = Transaction.all)
@scope = område
slut
def kald
visible(@scope)
.then(&method(:active))
.then(&method(:for_publishers))
.order(:created_at)
slut
privat
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("transaktioner.*")
.joins(:account)
.where("accounts.owner_type = 'Publisher'")
.joins("JOIN publishers ON owner_id = publishers.id")
slut
slut
Efter vores oprydning ser modellen helt klart lettere ud. Der fokuserer vi kun på datavalidering og relationer mellem andre modeller.
class Transaction < ActiveRecord::Base
hører til :konto
har_en :udbetalingspost
slut
Controlleren har allerede implementeret vores løsning; vi har flyttet alle yderligere forespørgsler til en separat klasse. Men de kald, vi ikke havde i modellen, er stadig et uløst problem. Efter nogle ændringer ser vores indekshandling sådan ud:
class TransactionsController < ApplikationsController
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)
slut
slut
Løsning
I tilfælde af implementering af god praksis og konventioner kan det være en god idé at erstatte alle lignende forekomster af et givet problem. Derfor flytter vi SQL-forespørgslen fra indekshandlingen til det separate forespørgselsobjekt. Vi vil kalde dette en TransactionsFilterableQuery
klasse. Stilen, som vi forbereder klassen i, vil ligne den, der præsenteres i TransactionsQuery
. Som en del af Kode ændringer, vil der blive smuglet en mere intuitiv registrering af store SQL-forespørgsler ved hjælp af flerlinjede tegnstrenge kaldet heredoc. Den tilgængelige løsning finder du nedenfor:
klasse TransactionsFilterableQuery
def initialize(scope = Transaction.all)
@scope = område
slut
def opkald
withdrawal(@scope).then(&method(:withdrawal_items))
end
privat
def tilbagetrækning(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
slut
def withdrawal_items(scope)
scope.joins(<<-SQL
LEFT JOIN withdrawal_items ON withdrawal_items.accounting_event_id = transactions.id
SQL
end
end
I tilfælde af ændringer i controlleren reducerer vi mængden af linjer ved at tilføje query-objektet. Det er vigtigt, at vi adskiller alt undtagen den del, der er ansvarlig for paginering.
class TransactionsController < ApplikationsController
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
slut
Sammenfatning
Query object ændrer meget på tilgangen til at skrive SQL-forespørgsler. I ActiveRecord er det meget nemt at placere al forretnings- og databaselogik i modellen, da alt er samlet ét sted. Det fungerer fint til mindre applikationer. Når projektets kompleksitet øges, placerer vi logikken andre steder. Det samme forespørgselsobjekt giver dig mulighed for at gruppere medlemsforespørgsler til et specifikt problem.
Takket være dette har vi en nem mulighed for senere nedarvning af koden, og på grund af duck typing kan du også bruge disse løsninger i andre modeller. Ulempen ved denne løsning er en større mængde kode og fragmentering af ansvaret. Men om vi vil tage sådan en udfordring op eller ej, afhænger af os selv og af, hvor meget vi bliver forstyrret af fede modeller.