Abfragen in Rails, Teil 3
German (Deutsch) translation by Valentina (you can also view the original English article)
In diesem letzten Stück werden wir uns etwas eingehender mit Abfragen befassen und mit einigen fortgeschritteneren Szenarien spielen. In diesem Artikel werden wir die Beziehungen von Active Record-Modellen etwas ausführlicher behandeln, aber ich werde mich von Beispielen fernhalten, die für die Programmierung von Neulingen zu verwirrend sein könnten. Bevor Sie fortfahren, sollten Dinge wie das folgende Beispiel keine Verwirrung stiften:
Mission.last.agents.where(name: 'James Bond')
Wenn Sie mit Active Record-Abfragen und SQL noch nicht vertraut sind, empfehlen wir Ihnen, sich meine beiden vorherigen Artikel anzusehen, bevor Sie fortfahren. Dieser könnte schwer zu schlucken sein, ohne das Wissen, das ich bisher aufgebaut habe. Natürlich bis zu dir. Auf der anderen Seite wird dieser Artikel nicht so lang sein wie die anderen, wenn Sie sich nur diese leicht fortgeschrittenen Anwendungsfälle ansehen möchten. Lassen Sie uns eintauchen!
Themen
- Bereiche und Verbände
- Slimmer Joins
- Merge
- has_many
- Custom Joins
Bereiche und Verbände
Lassen Sie uns noch einmal wiederholen. Wir können Active Record-Modelle sofort abfragen, aber Assoziationen sind auch ein faires Spiel für Abfragen - und wir können all diese Dinge verketten. So weit, ist es gut. Wir können Finder auch in Ihren Modellen in ordentliche, wiederverwendbare Bereiche packen, und ich habe kurz ihre Ähnlichkeit mit Klassenmethoden erwähnt.
Rails
class Agent < ActiveRecord::Base belongs_to :mission scope :find_bond, -> { where(name: 'James Bond') } scope :licenced_to_kill, -> { where(licence_to_kill: true) } scope :womanizer, -> { where(womanizer: true) } scope :gambler, -> { where(gambler: true) } end # => Agent.find_bond # => Agent.licenced_to_kill # => Agent.womanizer # => Agent.gambler # => Mission.last.agents.find_bond # => Mission.last.agents.licenced_to_kill # => Mission.last.agents.womanizer # => Mission.last.agents.gambler # => Agent.licenced_to_kill.womanizer.gambler # => Mission.last.agents.womanizer.gambler.licenced_to_kill
Sie können sie also auch in Ihre eigenen Klassenmethoden packen und damit fertig sein. Die Bereiche sind nicht zweifelhaft oder so, denke ich - obwohl die Leute sie hier und da als etwas magisch bezeichnen -, aber da Klassenmethoden dasselbe erreichen, würde ich mich dafür entscheiden.
Rails
class Agent < ActiveRecord::Base belongs_to :mission def self.find_bond where(name: 'James Bond') end def self.licenced_to_kill where(licence_to_kill: true) end def self.womanizer where(womanizer: true) end def self.gambler where(gambler: true) end end # => Agent.find_bond # => Agent.licenced_to_kill # => Agent.womanizer # => Agent.gambler # => Mission.last.agents.find_bond # => Mission.last.agents.licenced_to_kill # => Mission.last.agents.womanizer # => Mission.last.agents.gambler # => Agent.licenced_to_kill.womanizer.gambler # => Mission.last.agents.womanizer.gambler.licenced_to_kill
Diese Klassenmethoden lesen sich genauso und Sie müssen niemanden mit einem Lambda erstechen. Was auch immer für Sie oder Ihr Team am besten funktioniert; Es liegt an Ihnen, welche API Sie verwenden möchten. Kombinieren Sie sie einfach nicht - bleiben Sie bei der einmaligen Auswahl! Mit beiden Versionen können Sie diese Methoden einfach in eine andere Klassenmethode verketten, zum Beispiel:
Rails
class Agent < ActiveRecord::Base belongs_to :mission scope :licenced_to_kill, -> { where(licence_to_kill: true) } scope :womanizer, -> { where(womanizer: true) } def self.find_licenced_to_kill_womanizer womanizer.licenced_to_kill end end # => Agent.find_licenced_to_kill_womanizer # => Mission.last.agents.find_licenced_to_kill_womanizer
Rails
class Agent < ActiveRecord::Base belongs_to :mission def self.licenced_to_kill where(licence_to_kill: true) end def self.womanizer where(womanizer: true) end def self.find_licenced_to_kill_womanizer womanizer.licenced_to_kill end end # => Agent.find_licenced_to_kill_womanizer # => Mission.last.agents.find_licenced_to_kill_womanizer
Gehen wir noch einen Schritt weiter - bleiben Sie bei mir. Wir können ein Lambda in Assoziationen selbst verwenden, um einen bestimmten Umfang zu definieren. Es sieht auf den ersten Blick etwas komisch aus, aber sie können ziemlich praktisch sein. Das macht es möglich, diese Lambdas direkt in Ihren Verbänden zu nennen.
Dies ist ziemlich cool und gut lesbar, da kürzere Methoden verkettet werden. Achten Sie jedoch darauf, diese Modelle nicht zu fest zu koppeln.
Rails
class Mission < ActiveRecord::Base has_many :double_o_agents, -> { where(licence_to_kill: true) }, class_name: "Agent" end # => Mission.double_o_agents
Sagen Sie mir, das ist irgendwie nicht cool! Es ist nicht für den täglichen Gebrauch, aber ich denke, es ist doof. Hier kann Mission
also nur Agenten "anfordern", die die Lizenz zum Töten haben.
Ein Wort zur Syntax, da wir von Namenskonventionen abgewichen sind und etwas Ausdrucksvolleres wie double_o_agents
verwendet haben. Wir müssen den Klassennamen erwähnen, um Rails nicht zu verwirren, da sonst erwartet werden könnte, dass nach einer Klasse DoubleOAgent
gesucht wird. Sie können natürlich beide Agent
-Zuordnungen einrichten - die übliche und Ihre benutzerdefinierte - und Rails wird sich nicht beschweren.
Rails
class Mission < ActiveRecord::Base has_many :agents has_many :double_o__agents, -> { where(licence_to_kill: true) }, class_name: "Agent" end # => Mission.agents # => Mission.double_o_agents
Slimmer Joins
Wenn Sie die Datenbank nach Datensätzen abfragen und nicht alle Daten benötigen, können Sie angeben, was genau zurückgegeben werden soll. Warum? Da die an Active Record zurückgegebenen Daten schließlich in neue Ruby-Objekte integriert werden. Schauen wir uns eine einfache Strategie an, um ein Aufblähen des Speichers in Ihrer Rails-App zu vermeiden:
Rails
class Mission < ActiveRecord::Base has_many :agents end class Agent < ActiveRecord::Base belongs_to :mission end
Rails
Agent.all.joins(:mission)
SQL
SELECT "agents".* FROM "agents" INNER JOIN "missions" ON "missions"."id" = "agents"."mission_id"
Diese Abfrage gibt also eine Liste von Agenten mit einer Mission aus der Datenbank an Active Record zurück, die dann wiederum Ruby-Objekte daraus erstellen. Die mission
-Daten sind verfügbar, da die Daten aus diesen Zeilen mit den Zeilen der Agentendaten verknüpft werden. Das heißt, die verknüpften Daten sind während der Abfrage verfügbar, werden jedoch nicht an Active Record zurückgegeben. So haben Sie diese Daten zum Beispiel, um Berechnungen durchzuführen.
Es ist besonders cool, weil Sie Daten verwenden können, die nicht auch an Ihre App zurückgesendet werden. Weniger Attribute, die in Ruby-Objekte eingebaut werden müssen - die Speicherplatz beanspruchen - können ein großer Gewinn sein. Denken Sie im Allgemeinen daran, nur die absolut erforderlichen Zeilen und Spalten zurückzusenden, die Sie benötigen. Auf diese Weise können Sie ein Aufblähen vermeiden.
Rails
Agent.all.joins(:mission).where(missions: { objective: "Saving the world" })
Nur eine kurze Bemerkung zur Syntax hier: Da wir die Agent
-Tabelle nicht über where
, sondern über die join :mission
-Tabelle abfragen, müssen wir in unserer WHERE
-Klausel angeben, dass wir nach bestimmten missions
suchen.
SQL
SELECT "agents".* FROM "agents" INNER JOIN "missions" ON "missions"."id" = "agents"."mission_id" WHERE "missions"."objective" = ? [["objective", "Saving the world"]]
Die Verwendung von includes
hier würde auch Missionen zum eifrigen Laden an Active Record zurückgeben und den Speicheraufbau von Ruby-Objekten aufnehmen.
Merge
Eine merge
ist beispielsweise nützlich, wenn Sie eine Abfrage zu Agenten und den zugehörigen Missionen kombinieren möchten, die einen bestimmten von Ihnen definierten Bereich haben. Wir können zwei ActiveRecord::Relation
-Objekte nehmen und ihre Bedingungen zusammenführen. Sicher, kein Problem, aber merge
ist nützlich, wenn Sie einen bestimmten Bereich verwenden möchten, während Sie einen Verband verwenden.
Mit anderen Worten, was wir mit merge
tun können, ist das Filtern nach einem benannten Bereich im verbundenen Modell. In einem der vorherigen Beispiele haben wir Klassenmethoden verwendet, um solche benannten Bereiche selbst zu definieren.
Rails
class Mission < ActiveRecord::Base has_many :agents def self.dangerous where(enemy: "Ernst Stavro Blofeld") end end class Agent < ActiveRecord::Base belongs_to :mission end
Rails
Agent.joins(:mission).merge(Mission.dangerous)
SQL
SELECT "agents".* FROM "agents" INNER JOIN "missions" ON "missions"."id" = "agents"."mission_id" WHERE "missions"."enemy" = ? [["enemy", "Ernst Stavro Blofeld"]]
Wenn wir zusammenfassen, was eine dangerous
Mission im Mission
-Modell ist, können wir sie auf diese Weise durch merge
in einen join
einbinden. Die Logik solcher Bedingungen auf das relevante Modell zu verlagern, zu dem sie gehören, ist einerseits eine gute Technik, um eine lockere Kopplung zu erreichen - wir möchten nicht, dass unsere Active Record-Modelle viele Details über einander wissen - und andererseits Hand, es gibt Ihnen eine schöne API in Ihren Joins, ohne in die Luft zu jagen. Das folgende Beispiel ohne Zusammenführung würde ohne Fehler nicht funktionieren:
Rails
Agent.all.merge(Mission.dangerous)
SQL
SELECT "agents".* FROM "agents" WHERE "missions"."enemy" = ? [["enemy", "Ernst Stavro Blofeld"]]
Wenn wir jetzt ein ActiveRecord::Relation
-Objekt für unsere Missionen auf unseren Agenten zusammenführen, weiß die Datenbank nicht, um welche Missionen es sich handelt. Wir müssen klarstellen, welcher Verband wir benötigen, und uns zuerst den Missionsdaten anschließen - sonst wird SQL verwirrt. Eine letzte Kirsche oben drauf. Wir können dies noch besser zusammenfassen, indem wir auch die Agenten einbeziehen:
Rails
class Mission < ActiveRecord::Base has_many :agents def self.dangerous where(enemy: "Ernst Stavro Blofeld") end end class Agent < ActiveRecord::Base belongs_to :mission def self.double_o_engagements joins(:mission).merge(Mission.dangerous) end end
Rails
Agent.double_o_engagements
SQL
SELECT "agents".* FROM "agents" INNER JOIN "missions" ON "missions"."id" = "agents"."mission_id" WHERE "missions"."enemy" = ? [["enemy", "Ernst Stavro Blofeld"]]
Das ist eine süße Kirsche in meinem Buch. Kapselung, korrekter OOP und gute Lesbarkeit. Jackpot!
has_many
Oben haben wir den belongs_to
-Verband viel in Aktion gesehen. Schauen wir uns das aus einer anderen Perspektive an und bringen wir Geheimdienstabschnitte in den Mix:
Rails
class Section < ActiveRecord::Base has_many :agents end class Mission < ActiveRecord::Base has_many :agents end class Agent < ActiveRecord::Base belongs_to :mission belongs_to :section end
In diesem Szenario hätten Agenten also nicht nur eine mission_id
, sondern auch eine section_id
. So weit, ist es gut. Lassen Sie uns alle Abschnitte mit Agenten mit einer bestimmten Mission finden - also Abschnitte, in denen eine Mission ausgeführt wird.
Rails
Section.joins(:agents)
SQL
SELECT "sections".* FROM "sections" INNER JOIN "agents" ON "agents"."section_id" = "sections."id"
Haben Sie etwas bemerkt? Ein kleines Detail ist anders. Die Fremdschlüssel werden umgedreht. Hier fordern wir eine Liste von Abschnitten an, verwenden jedoch Fremdschlüssel wie diesen: "agents". "section_id" = "sections." id "
. Mit anderen Worten, wir suchen nach einem Fremdschlüssel aus einer Tabelle, der wir beitreten.
Rails
Agent.joins(:mission)
SQL
SELECT "agents".* FROM "agents" INNER JOIN "missions" ON "missions"."id" = "agents"."mission_id"
Bisher sahen unsere Verknüpfungen über eine belongs_to
-Zuordnung folgendermaßen aus: Die Fremdschlüssel wurden gespiegelt ("missions"."id" = "agents"."mission_id"
) und suchten nach dem Fremdschlüssel aus der Tabelle, die wir starten.
Wenn wir zu Ihrem has_many
-Szenario zurückkehren, erhalten wir jetzt eine Liste von Abschnitten, die wiederholt werden, da sie natürlich mehrere Agenten in jedem Abschnitt haben. Für jede Agentenspalte, die verbunden wird, erhalten wir eine Zeile für diesen Abschnitt oder diese section_id. Kurz gesagt, wir duplizieren grundsätzlich Zeilen. Um dies noch schwindelerregender zu machen, bringen wir auch Missionen in den Mix.
Rails
Section.joins(agents: :mission)
SQL
SELECT "sections".* FROM "sections" INNER JOIN "agents" ON "agents"."section_id" = "sections"."id" INNER JOIN "missions" ON "missions"."id" = "agents"."mission_id"
Schauen Sie sich die beiden INNER JOIN
-Teile an. Immer noch bei mir? Wir "erreichen" durch Agenten ihre Missionen aus der Agentenabteilung. Ja, Sachen zum Spaß, ich weiß. Was wir bekommen, sind Missionen, die indirekt mit einem bestimmten Abschnitt verbunden sind.
Infolgedessen werden neue Spalten hinzugefügt, aber die Anzahl der Zeilen ist immer noch dieselbe, die von dieser Abfrage zurückgegeben wird. Was an Active Record zurückgesendet wird, was zum Erstellen neuer Ruby-Objekte führt, ist auch weiterhin die Liste der Abschnitte. Wenn also mehrere Missionen mit mehreren Agenten ausgeführt werden, erhalten wir erneut doppelte Zeilen für unseren Abschnitt. Lassen Sie uns dies noch etwas filtern:
Rails
Section.joins(agents: :mission).where(missions: { enemy: "Ernst Stavro Blofeld" })
SQL
SELECT "sections".* FROM "sections" INNER JOIN "agents" ON "agents"."section_id" = "sections"."id" INNER JOIN "missions" ON "missions"."id" = "agents"."mission_id" WHERE "missions"."enemy" = 'Ernst Stavro Blofeld'
Jetzt erhalten wir nur Abschnitte zurück, die an Missionen beteiligt sind, bei denen Ernst Stavro Blofeld der betroffene Feind ist. Kosmopolitisch, wie manche Superschurken vielleicht von sich denken, könnten sie in mehr als einem Abschnitt operieren - sagen wir Abschnitt A und C, die Vereinigten Staaten bzw. Kanada.
Wenn wir in einem bestimmten Abschnitt mehrere Agenten haben, die an derselben Mission arbeiten, um Blofeld oder was auch immer zu stoppen, hätten wir wieder wiederholte Zeilen in Active Record. Lassen Sie uns etwas genauer darauf eingehen:
Rails
Section.joins(agents: :mission).where(missions: { enemy: "Ernst Stavro Blofeld" }).distinct
SQL
SELECT DISTINCT "sections".* FROM "sections" INNER JOIN "agents" ON "agents"."section_id" = "sections"."id" INNER JOIN "missions" ON "missions"."id" = "agents"."mission_id" WHERE "missions"."enemy" = 'Ernst Stavro Blofeld'
Dies gibt uns die Anzahl der Abschnitte, in denen Blofeld operiert - die bekannt sind -, in denen Agenten in Missionen mit ihm als Feind aktiv sind. Lassen Sie uns als letzten Schritt noch einmal einige Umgestaltungen vornehmen. Wir extrahieren dies in eine nette "kleine" Klassenmethode im class Section
:
Rails
class Section < ActiveRecord::Base has_many :agents def self.critical joins(agents: :mission).where(missions: { enemy: "Ernst Stavro Blofeld" }).distinct end end class Mission < ActiveRecord::Base has_many :agents end class Agent < ActiveRecord::Base belongs_to :mission belongs_to :section end
Sie können dies noch weiter umgestalten und die Verantwortlichkeiten aufteilen, um eine lockerere Kopplung zu erreichen. Lassen Sie uns jedoch vorerst fortfahren.
Custom Joins
Meistens können Sie sich darauf verlassen, dass Active Record das gewünschte SQL schreibt. Das bedeutet, dass Sie in Ruby Land bleiben und sich nicht zu viele Gedanken über Datenbankdetails machen müssen. Aber manchmal müssen Sie ein Loch in das SQL-Land stecken und Ihr eigenes Ding machen. Zum Beispiel, wenn Sie einen LEFT
Join verwenden und aus dem üblichen Verhalten von Active Record ausbrechen müssen, einen INNER
-Join standardmäßig durchzuführen. joins
ist ein kleines Fenster, in dem Sie bei Bedarf Ihr eigenes benutzerdefiniertes SQL schreiben können. Sie öffnen es, fügen Ihren benutzerdefinierten Abfragecode ein, schließen das „Fenster“ und können weiterhin Active Record-Abfragemethoden hinzufügen.
Lassen Sie uns dies anhand eines Beispiels demonstrieren, das Gadgets umfasst. Nehmen wir an, ein typischer Agent normalerweise has_many
Gadgets, und wir möchten Agenten finden, die nicht mit ausgefallenen Gadgets ausgestattet sind, um ihnen vor Ort zu helfen. Ein gewöhnlicher Join würde keine guten Ergebnisse liefern, da wir tatsächlich an nil
- oder null
in SQL - Werten dieser Spionagespielzeuge interessiert sind.
Rails
class Mission < ActiveRecord::Base has_many :agents end class Agent < ActiveRecord::Base belongs_to :mission has_many :gadgets end class Gadget < ActiveRecord::Base belongs_to :agent end
Wenn wir eine joins
-Operation ausführen, werden nur Agenten zurückgegeben, die bereits mit Gadgets ausgestattet sind, da die agent_id
für diese Gadgets nicht Null ist. Dies ist das erwartete Verhalten eines Standard-Inner-Joins. Der innere Join baut auf einer Übereinstimmung auf beiden Seiten auf und gibt nur Datenzeilen zurück, die dieser Bedingung entsprechen. Ein nicht vorhandenes Gadget mit dem Wert nil
für einen Agenten, der kein Gadget enthält, entspricht nicht diesem Kriterium.
Rails
Agent.joins(:gadgets)
SQL
SELECT "agents".* FROM "agents" INNER JOIN "gadgets" ON "gadgets"."agent_id" = "agents"."id"
Auf der anderen Seite suchen wir nach Schmuck-Agenten, die dringend etwas Liebe vom Quartiermeister brauchen. Ihre erste Vermutung könnte wie folgt ausgesehen haben:
Rails
Agent.joins(:gadgets).where(gadgets: {agent_id: nil})
SQL
SELECT "agents".* FROM "agents" INNER JOIN "gadgets" ON "gadgets"."agent_id" = "agents"."id" WHERE "gadgets"."agent_id" IS NULL
Nicht schlecht, aber wie Sie der SQL-Ausgabe entnehmen können, spielt sie nicht mit und besteht weiterhin auf der Standardeinstellung INNER JOIN
. Dies ist ein Szenario, in dem wir einen OUTER
-Join benötigen, da sozusagen eine Seite unserer „Gleichung“ fehlt. Wir suchen nach Ergebnissen für nicht vorhandene Gadgets - genauer gesagt für Agenten ohne Gadgets.
Als wir bisher in einem Join ein Symbol an Active Record übergeben haben, hat es eine Zuordnung erwartet. Wenn jedoch eine Zeichenfolge übergeben wird, wird erwartet, dass es sich um ein tatsächliches Fragment von SQL-Code handelt - ein Teil Ihrer Abfrage.
Rails
Agent.joins("LEFT OUTER JOIN gadgets ON gadgets.agent_id = agents.id").where(gadgets: {agent_id: nil})
SQL
SELECT "agents".* FROM "agents" LEFT OUTER JOIN gadgets ON gadgets.agent_id = agents.id WHERE "gadgets"."agent_id" IS NULL
Oder wenn Sie neugierig auf faule Agenten ohne Mission sind - möglicherweise auf Barbados oder wo auch immer -, würde unser benutzerdefinierter Join folgendermaßen aussehen:
Rails
Agent.joins("LEFT OUTER JOIN missions ON missions.id = agents.mission_id").where(missions: { id: nil })
SQL
SELECT "agents".* FROM "agents" LEFT OUTER JOIN missions ON missions.id = agents.mission_id WHERE "missions"."id" IS NULL
Die äußere Verknüpfung ist die umfassendere Verknüpfungsversion, da sie mit allen Datensätzen aus den verknüpften Tabellen übereinstimmt, auch wenn einige dieser Beziehungen noch nicht vorhanden sind. Da dieser Ansatz nicht so exklusiv ist wie innere Verknüpfungen, erhalten Sie hier und da eine Reihe von Nullen. Dies kann natürlich in einigen Fällen informativ sein, aber innere Verknüpfungen sind normalerweise das, wonach wir suchen. In Rails 5 können wir stattdessen eine spezielle Methode namens left_outer_joins
für solche Fälle verwenden. Schließlich!
Eine Kleinigkeit für die Straße: Halten Sie diese Gucklöcher im SQL-Land so klein wie möglich, wenn Sie können. Sie werden jedem - einschließlich Ihres zukünftigen Selbst - einen enormen Gefallen tun.
Abschließende Gedanken
Active Record dazu zu bringen, effizientes SQL für Sie zu schreiben, ist eine der Hauptfähigkeiten, die Sie dieser Miniserie für Anfänger abnehmen sollten. Auf diese Weise erhalten Sie auch Code, der mit der unterstützten Datenbank kompatibel ist. Dies bedeutet, dass die Abfragen datenbankübergreifend stabil sind. Sie müssen nicht nur verstehen, wie man mit Active Record spielt, sondern auch das zugrunde liegende SQL, das von gleicher Bedeutung ist.
Ja, SQL kann langweilig sein, mühsam zu lesen sein und nicht elegant aussehen. Vergessen Sie jedoch nicht, dass Rails Active Record mit SQL umschließt, und Sie sollten das Verständnis dieser wichtigen Technologie nicht vernachlässigen - nur weil Rails es sehr einfach macht, sich nicht darum zu kümmern der ganzen Zeit. Effizienz ist für Datenbankabfragen von entscheidender Bedeutung, insbesondere wenn Sie etwas für ein größeres Publikum mit starkem Datenverkehr erstellen.
Gehen Sie jetzt ins Internet und suchen Sie nach mehr Material zu SQL, um es ein für alle Mal aus Ihrem System zu entfernen!