Det är ganska troligt att du på jobbet har stött på överbelastade modeller och ett stort antal anrop i styrenheterna ganska många gånger. Baserat på kunskapen i Rails-miljön kommer jag i den här artikeln att föreslå en enkel lösning på detta problem.
En mycket viktig aspekt av Rails-applikationen är att minimera antalet redundanta beroenden, vilket är anledningen till att hela Rails-miljön nyligen har främjat serviceobjektmetoden och användningen av PORO-metoden (Pure Old Ruby Object). En beskrivning av hur man använder en sådan lösning hittar du här. I den här artikeln kommer vi att lösa konceptet steg för steg och anpassa det till problemet.
Problem
I en hypotetisk applikation har vi att göra med ett komplicerat transaktionssystem. Vår modell, som representerar varje transaktion, har en uppsättning scopes som hjälper dig att hämta data. Det underlättar arbetet eftersom det finns på ett och samma ställe. Detta varar dock inte länge. I och med utvecklingen av applikationen projekt blir mer och mer komplicerat. Scopes har inte längre enkla "var"-referenser, vi saknar data och börjar ladda relationer. Efter ett tag påminner det om ett komplicerat system av speglar. Och, vad värre är, vi vet inte hur man gör en lambda med flera rader!
Nedan hittar du en redan utökad applikationsmodell. Betalningssystemets transaktioner lagras i. Som du kan se i exemplet nedan:
klass Transaktion { where(visible: true) }
scope(:active, lambda do
sammanfogar(<<-SQL
LEFT OUTER JOIN source ON transactions.source_id = source.id
AND source.accepted_at IS NOT NULL
SQL
slut)
slut
Modellen är en sak, men när skalan på vårt projekt ökar börjar även controllers att svälla. Låt oss titta på exemplet nedan:
klass TransactionsController < ApplikationsController
def index
@transactions = Transaction.for_publishers
.aktiv
.synlig
.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])
@transaktioner = tillämpa_filter(@transaktioner)
slut
slut
Här kan vi se många rader av kedjade metoder tillsammans med ytterligare sammanfogningar som vi inte vill utföra på många ställen, bara på just detta. De bifogade uppgifterna används senare av metoden apply_filters, som lägger till lämplig datafiltrering, baserat på GET-parametrarna. Naturligtvis kan vi överföra några av dessa referenser till scope, men är det inte det här problemet som vi faktiskt försöker lösa?
Lösning
Eftersom vi redan vet att vi har ett problem måste vi lösa det. Baserat på referensen i inledningen kommer vi här att använda PORO-metoden. I just det här fallet kallas metoden för query object, vilket är en utveckling av konceptet service objects.
Låt oss skapa en ny katalog med namnet "services", som ligger i appskatalogen i vårt projekt. Där kommer vi att skapa en klass med namnet TransactionsQuery
.
klass TransactionsQuery
slut
Som ett nästa steg måste vi skapa en initialiserare där en standardanropsväg för vårt objekt kommer att skapas
klass TransactionsQuery
def initialize(scope = Transaktion.all)
@scope = omfattning
slut
slut
Tack vare detta kommer vi att kunna överföra relationen från den aktiva posten till vår anläggning. Nu kan vi överföra alla våra scopes till klassen, som bara behövs i den presenterade styrenheten.
klass TransactionsQuery
def initialize(scope = Transaktion.all)
@scope = omfattning
slut
privat
def aktiv(omfattning)
scope.joins(<<-SQL
LEFT OUTER JOIN source ON transactions.source_id = source.id
AND source.accepted_at IS NOT NULL
SQL
slut
def synlig(omfattning)
scope.where(synlig: true)
slut
def for_publishers(omfattning)
scope.select("transaktioner.*")
.joins(:konto)
.where("accounts.owner_type = 'Publisher'")
.joins("JOIN publishers ON owner_id = publishers.id")
slut
slut
Vi saknar fortfarande den viktigaste delen, dvs. att samla data i en sträng och göra gränssnittet publikt. Metoden där vi sammanfogar allt kommer att kallas "call".
Det som verkligen är viktigt är att vi kommer att använda instansvariabeln @scope där, där omfattningen av vårt anrop finns.
klass TransactionsQuery
...
def anrop
synlig(@scope)
.then(&metod(:aktiv))
.then(&method(:for_publishers))
.order(:created_at)
slut
privat
...
slut
Hela klassen presenterar sig som följande:
klass TransactionsQuery
def initialize(scope = Transaktion.all)
@scope = omfattning
slut
def anrop
synlig(@scope)
.then(&metod(:aktiv))
.then(&method(:for_publishers))
.order(:created_at)
slut
privat
def aktiv(omfattning)
scope.joins(<<-SQL
LEFT OUTER JOIN source ON transactions.source_id = source.id
AND source.accepted_at IS NOT NULL
SQL
slut
def synlig(omfattning)
scope.where(synlig: true)
slut
def for_publishers(omfattning)
scope.select("transaktioner.*")
.joins(:konto)
.where("accounts.owner_type = 'Publisher'")
.joins("JOIN publishers ON owner_id = publishers.id")
slut
slut
Efter vår upprensning ser modellen definitivt lättare ut. Där fokuserar vi bara på datavalideringen och relationerna mellan andra modeller.
klass Transaktion < ActiveRecord::Bas
tillhör_till :konto
har_ett :uttag_objekt
slut
Styrenheten har redan implementerat vår lösning; vi har flyttat alla ytterligare frågor till en separat klass. De anrop som vi inte hade i modellen är dock fortfarande ett olöst problem. Efter några ändringar ser vår indexåtgärd ut så här:
klass TransactionsController < ApplikationsController
def index
@transaktioner = 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])
@transaktioner = tillämpa_filter(@transaktioner)
slut
slut
Lösning
När det gäller att implementera god praxis och konventioner kan det vara en bra idé att ersätta alla liknande förekomster av ett visst problem. Därför kommer vi att flytta SQL-frågan från indexåtgärden till det separata frågeobjektet. Vi kommer att kalla detta för en TransactionsFilterableQuery
klass. Stilen, som vi förbereder klassen i, kommer att likna den som presenteras i TransactionsQuery
. Som en del av kod kommer en mer intuitiv registrering av stora SQL-frågor att smugglas in, med hjälp av flerradiga teckensträngar som kallas heredoc. Den lösning som finns tillgänglig hittar du nedan:
klass TransactionsFilterableQuery
def initialize(scope = Transaktion.all)
@scope = omfattning
slut
def anrop
withdrawal(@scope).then(&method(:withdrawal_items))
slut
privat
def uttag(omfattning)
scope.joins(<<-SQL
LEFT JOIN withdrawals ON withdrawals.id = withdrawal_items.withdrawal_id OR
(withdrawals.id = source.resource_id AND source.resource_type = 'Uttag')
SQL
slut
def uttag_items(omfattning)
scope.joins(<<-SQL
LEFT JOIN withdrawal_items ON withdrawal_items.accounting_event_id = transactions.id
SQL
slut
slut
Vid ändringar i styrenheten minskar vi mängden rader genom att lägga till frågeobjektet. Det är viktigt att vi separerar allt utom den del som ansvarar för paginering.
klass TransactionsController < ApplikationsController
def index
@transactions = TransactionsQuery.new.call.then do |scope|
TransactionsFilterableQuery.new(scope).call
end.page(params[:page]).per(params[:page])
@transaktioner = tillämpa_filter(@transaktioner)
slut
slut
Sammanfattning
Query object förändrar mycket i sättet att skriva SQL-frågor. I ActiveRecord är det mycket enkelt att placera all affärs- och databaslogik i modellen eftersom allt finns på ett och samma ställe. Detta kommer att fungera bra för mindre applikationer. När komplexiteten i projektet ökar lägger vi logiken på andra ställen. Samma frågeobjekt gör att du kan gruppera medlemsfrågor till ett specifikt problem.
Tack vare detta har vi en enkel möjlighet till senare nedärvning av koden och på grund av duck typing kan du även använda dessa lösningar i andra modeller. Nackdelen med denna lösning är en större mängd kod och fragmentering av ansvaret. Men om vi vill ta oss an en sådan utmaning eller inte beror på oss själva och hur mycket vi störs av feta modeller.