Es ist sehr wahrscheinlich, dass Sie bei Ihrer Arbeit schon oft mit überladenen Modellen und einer großen Anzahl von Aufrufen in den Controllern zu tun hatten. Basierend auf dem Wissen in der Rails-Umgebung, in diesem Artikel, werde ich eine einfache Lösung für dieses Problem vorschlagen.
Ein sehr wichtiger Aspekt der Rails-Anwendung ist es, die Anzahl der redundanten Abhängigkeiten zu minimieren, weshalb die gesamte Rails-Umgebung in letzter Zeit den Service-Objekt-Ansatz und die Verwendung der PORO-Methode (Pure Old Ruby Object) fördert. Eine Beschreibung, wie man eine solche Lösung einsetzt, finden Sie hier. In diesem Artikel werden wir das Konzept Schritt für Schritt lösen und es an das Problem anpassen.
Problem
In einer hypothetischen Anwendung haben wir es mit einem komplizierten Transaktionssystem zu tun. Unser Modell, das jede Transaktion repräsentiert, hat eine Reihe von Bereichen, die Ihnen helfen, Daten zu erhalten. Das ist eine große Arbeitserleichterung, da sie an einer Stelle zu finden sind. Dies gilt jedoch nicht für lange Zeit. Mit der Entwicklung der Anwendung werden die Projekt wird immer komplizierter. Die Bereiche haben keine einfachen "Wo"-Verweise mehr, es fehlen Daten und wir beginnen, Beziehungen zu laden. Nach einer Weile erinnert es an ein kompliziertes System von Spiegeln. Und, was noch schlimmer ist, wir wissen nicht, wie man ein mehrzeiliges Lambda erstellt!
Im Folgenden finden Sie ein bereits erweitertes Anwendungsmodell. Die Transaktionen des Zahlungssystems werden in gespeichert. Wie Sie im Beispiel unten sehen können:
Klasse Transaktion { 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
Das Modell ist eine Sache, aber wenn der Umfang unseres Projekts zunimmt, werden auch die Controller immer größer. Schauen wir uns das folgende Beispiel an:
class TransactionsController < AnwendungsController
def index
@transactions = Transaction.for_publishers
.aktiv
.visible
.joins("LEFT JOIN Abhebungen_Einträge ON Abhebungen_Einträge.transaction_id = Transaktionen.id")
.joins("LEFT JOIN Abhebungen ON Abhebungen.id = Abhebungen_Einzelheiten.Abhebungs_id OR
(abhebungen.id = quelle.ressource_id AND quelle.ressource_type = 'Abhebung')")
.order(:created_at)
.page(params[:page])
.per(params[:page])
@Transaktionen = apply_filters(@Transaktionen)
end
end
Hier sehen wir viele Zeilen verketteter Methoden mit zusätzlichen Verknüpfungen, die wir nicht an vielen Stellen durchführen wollen, sondern nur an dieser einen. Die angehängten Daten werden später von der apply_filters-Methode verwendet, die auf der Grundlage der GET-Parameter die entsprechende Datenfilterung vornimmt. Natürlich können wir einige dieser Verweise in den Anwendungsbereich verlagern, aber ist das nicht das Problem, das wir eigentlich zu lösen versuchen?
Lösung
Da wir bereits wissen, dass wir ein Problem haben, müssen wir dieses lösen. Basierend auf dem Hinweis in der Einleitung werden wir hier den PORO-Ansatz verwenden. In diesem konkreten Fall wird dieser Ansatz als Abfrageobjekt bezeichnet, das eine Weiterentwicklung des Konzepts der Serviceobjekte ist.
Legen wir ein neues Verzeichnis mit dem Namen "services" an, das sich im apps-Verzeichnis unseres Projekts befindet. Dort werden wir eine Klasse namens TransactionsQuery
.
Klasse TransactionsQuery
end
Als nächsten Schritt müssen wir einen Initialisierer erstellen, in dem ein Standardaufrufpfad für unser Objekt erstellt wird
class TransactionsQuery
def initialize(bereich = Transaction.all)
@scope = Bereich
end
end
Dadurch können wir die Beziehung vom aktiven Datensatz zu unserer Einrichtung übertragen. Jetzt können wir alle unsere Bereiche in die Klasse übertragen, die nur in dem vorgestellten Controller benötigt werden.
class TransactionsQuery
def initialize(bereich = Transaction.all)
@scope = Bereich
end
privat
def active(Bereich)
scope.joins(<<-SQL
LEFT OUTER JOIN source ON transactions.source_id = source.id
AND source.accepted_at IS NOT NULL
SQL
end
def visible(Bereich)
Bereich.where(sichtbar: wahr)
end
def for_publishers(bereich)
Bereich.select("Transaktionen.*")
.joins(:Konto)
.where("konto.eigentümer_typ = 'Herausgeber'")
.joins("JOIN verleger ON eigentümer_id = verleger.id")
end
end
Es fehlt noch der wichtigste Teil, nämlich das Sammeln von Daten in einer Zeichenkette und die Veröffentlichung der Schnittstelle. Die Methode, mit der wir alles zusammenfügen, wird "Aufruf" genannt.
Wirklich wichtig ist, dass wir dort die Instanzvariable @scope verwenden, in der sich der Bereich unseres Aufrufs befindet.
Klasse TransactionsQuery
...
def call
visible(@scope)
.then(&Methode(:aktiv))
.then(&Methode(:for_publishers))
.order(:created_at)
end
privat
...
end
Die gesamte Klasse stellt sich wie folgt dar:
class TransactionsQuery
def initialize(bereich = Transaction.all)
@scope = Bereich
end
def Aufruf
visible(@scope)
.then(&Methode(:aktiv))
.then(&Methode(:for_publishers))
.order(:created_at)
end
privat
def active(Bereich)
scope.joins(<<-SQL
LEFT OUTER JOIN source ON transactions.source_id = source.id
AND source.accepted_at IS NOT NULL
SQL
end
def visible(Bereich)
Bereich.where(sichtbar: wahr)
end
def for_publishers(bereich)
Bereich.select("Transaktionen.*")
.joins(:Konto)
.where("konto.eigentümer_typ = 'Herausgeber'")
.joins("JOIN verleger ON eigentümer_id = verleger.id")
end
end
Nach unserer Bereinigung sieht das Modell deutlich heller aus. Hier konzentrieren wir uns nur noch auf die Datenvalidierung und die Beziehungen zwischen anderen Modellen.
Klasse Transaktion < ActiveRecord::Base
gehört_zu :Konto
hat_einen :Abhebungsposten
end
Der Controller hat unsere Lösung bereits implementiert; wir haben alle zusätzlichen Abfragen in eine separate Klasse verschoben. Allerdings bleiben die Aufrufe, die wir nicht im Modell hatten, ein ungelöstes Problem. Nach einigen Änderungen sieht unsere Indexaktion wie folgt aus:
class TransactionsController < AnwendungsController
def index
@transactions = TransactionsQuery.new
.call
.joins("LEFT JOIN Abhebung_Einträge ON Abhebung_Einträge.Abrechnungs_Ereignis_id = Transaktionen.id")
.joins("LEFT JOIN Abhebungen ON abhebungen.id = abhebungen_positionen.abhebungs_id OR
(abhebungen.id = quelle.ressource_id AND quelle.ressource_type = 'abhebung')")
.order(:created_at)
.page(params[:page])
.per(params[:page])
@Transaktionen = apply_filters(@Transaktionen)
end
end
Lösung
Bei der Umsetzung guter Praktiken und Konventionen kann es eine gute Idee sein, alle ähnlichen Vorkommen eines bestimmten Problems zu ersetzen. Daher werden wir die SQL-Abfrage von der Indexaktion in das separate Abfrageobjekt verschieben. Wir nennen dies ein TransactionsFilterableQuery
Klasse. Der Stil, in dem wir den Unterricht vorbereiten, wird ähnlich sein wie der, der in TransactionsQuery
. Als Teil der Code ändert, wird eine intuitivere Aufzeichnung großer SQL-Abfragen unter Verwendung mehrzeiliger Zeichenketten namens heredoc. Die verfügbare Lösung finden Sie unten:
class TransactionsFilterableQuery
def initialize(scope = Transaction.all)
@scope = Bereich
end
def call
withdrawal(@scope).then(&method(:withdrawal_items))
end
privat
def entnahme(bereich)
bereich.joins(<<-SQL
LEFT JOIN Abhebungen ON abhebungen.id = abhebungen_items.abhebungen_id OR
(entnahmen.id = quelle.ressource_id AND quelle.ressource_type = 'entnahme')
SQL
end
def entnahme_artikel(bereich)
bereich.joins(<<-SQL
LEFT JOIN abhebung_positionen ON abhebung_positionen.abrechnung_ereignis_id = vorgänge.id
SQL
end
end
Bei Änderungen in der Steuerung reduzieren wir die Masse der Zeilen, indem wir das Abfrageobjekt hinzufügen. Es ist wichtig, dass wir alles außer dem Teil, der für die Paginierung verantwortlich ist, trennen.
class TransactionsController < AnwendungsController
def index
@transactions = TransactionsQuery.new.call.then do |scope|
TransactionsFilterableQuery.new(Geltungsbereich).call
end.page(params[:page]).per(params[:page])
@Transaktionen = apply_filters(@Transaktionen)
end
end
Zusammenfassung
Das Query-Objekt ändert viel an der Vorgehensweise beim Schreiben von SQL-Abfragen. In ActiveRecord ist es sehr einfach, die gesamte Geschäfts- und Datenbanklogik im Modell zu platzieren, da sich alles an einem Ort befindet. Dies ist für kleinere Anwendungen sehr gut geeignet. Mit zunehmender Komplexität des Projekts verlagern wir die Logik an andere Stellen. Mit dem gleichen Abfrageobjekt können Sie Abfragen von Mitgliedern zu einem bestimmten Problem gruppieren.
Dadurch haben wir eine einfache Möglichkeit der späteren Vererbung des Codes und aufgrund der Duck-Typisierung können Sie diese Lösungen auch in anderen Modellen verwenden. Der Nachteil dieser Lösung ist eine größere Menge an Code und eine Fragmentierung der Verantwortung. Ob wir eine solche Herausforderung annehmen wollen oder nicht, hängt jedoch von uns selbst ab und davon, wie sehr wir uns an fetten Modellen stören.