Abfragen in Rails, Teil 3

In diesem letzten Teil werden wir ein wenig tiefer in Abfragen schauen und mit ein paar fortgeschritteneren Szenarien spielen. In diesem Artikel werden wir die Beziehungen zwischen Active Record-Modellen etwas näher behandeln, aber ich werde mich von Beispielen fernhalten, die für die Programmierung von Neulingen zu verwirrend sein könnten. Bevor Sie sich vorwärts bewegen, sollten Dinge wie das folgende Beispiel keine Verwirrung verursachen:

Mission.last.agents.where (Name: 'James Bond')

Wenn Sie mit Active Record-Abfragen und SQL noch nicht vertraut sind, empfehle ich Ihnen, meine beiden vorherigen Artikel zu lesen, bevor Sie fortfahren. Dieser könnte schwer zu schlucken sein ohne das Wissen, das ich bis jetzt aufgebaut habe. Bis zu Ihnen natürlich. Auf der anderen Seite wird dieser Artikel nicht so lang sein wie die anderen, wenn Sie nur diese etwas fortgeschrittenen Anwendungsfälle betrachten möchten. Lass uns reinschauen! 

Themen

  • Geltungsbereiche und Assoziationen
  • Schlankere Joins
  • Verschmelzen
  • hat viele
  • Benutzerdefinierte Joins

Geltungsbereiche und Assoziationen

Wiederholen wir es. 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 ordentlichen, wiederverwendbaren Bereichen in Ihre Modelle packen, und ich erwähnte kurz ihre Ähnlichkeit mit Klassenmethoden.

Schienen

Klasse Agent < ActiveRecord::Base belongs_to :mission scope :find_bond, -> Where (Name: 'James Bond') Geltungsbereich: licenced_to_kill, -> Where (Lizenz_to_kill: true) Gültigkeitsbereich: womanizer, -> where (womanizer: true) Gültigkeitsbereich: Spieler, -> 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

So können Sie sie auch in Ihre eigenen Klassenmethoden packen und damit fertig sein. Ich denke nicht, dass die Anwendungsbereiche fragwürdig sind, obwohl die Leute hier und da etwas magisch sind. Aber da die Klassenmethoden das gleiche erreichen, würde ich mich dafür entscheiden.

Schienen

Klasse 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 für Sie oder Ihr Team am besten funktioniert; Es liegt an Ihnen, welche API Sie verwenden möchten. Kombinieren Sie sie nicht einfach mit einer einmaligen Wahl! Mit beiden Versionen können Sie diese Methoden problemlos in einer anderen Klassenmethode verketten, beispielsweise:

Schienen

Klasse Agent < ActiveRecord::Base belongs_to :mission scope :licenced_to_kill, -> where (licence_to_kill: true) Gültigkeitsbereich: womanizer, -> where (womanizer: true) def self.find_licenced_to_kill_womanizer womanizer.licenced_to_kill end end # # => Agent.find_licenced_to_kill_womanizer # => Mission.agents.find_licenced__to

Schienen

Klasse 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

Lassen Sie uns noch einen kleinen Schritt weitergehen, bleiben Sie bei mir. Wir können ein Lambda in Assoziationen verwenden, um einen bestimmten Umfang zu definieren. Es sieht auf den ersten Blick etwas komisch aus, aber sie können sehr praktisch sein. Das macht es möglich, diese Lambdas direkt in Ihren Assoziationen anzurufen. 

Dies ist ziemlich cool und gut lesbar, wobei kürzere Methoden verkettet werden. Hüten Sie sich jedoch, diese Modelle zu eng zu koppeln.

Schienen

Klasse Mission < ActiveRecord::Base has_many :double_o_agents, -> where (licence_to_kill: true), class_name: "Agent" end # => Mission.double_o_agents

Sag mir das ist irgendwie nicht cool! Es ist nicht für den alltäglichen Gebrauch gedacht, aber ich glaube, Dope. Also hier Mission kann nur Agenten anfordern, die die Lizenz zum Töten haben. 

Ein Wort zur Syntax, da wir uns von Namenskonventionen abgewandt haben und etwas Ausdrucksvolleres verwenden double_o_agents. Wir müssen den Klassennamen erwähnen, um Rails nicht zu verwirren, was andernfalls erwarten könnte, nach einer Klasse zu suchen DoubleOAgent. Sie können natürlich beides haben Agent Assoziationen an Ort und Stelle - die üblichen und Ihre eigenen - und Rails werden sich nicht beklagen.

Schienen

Klasse 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

Schlankere 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? Die an Active Record zurückgegebenen Daten werden schließlich in neue Ruby-Objekte integriert. Schauen wir uns eine einfache Strategie an, um Speicherüberlauf in Ihrer Rails-App zu vermeiden:

Schienen

Klasse Mission < ActiveRecord::Base has_many :agents end class Agent < ActiveRecord::Base belongs_to :mission end

Schienen

Agent.all.joins (: mission)

SQL

SELECT "agents". * FROM "agents" INNER JOIN "-Missionen" ON "-Missionen". "Id" = "Agenten". "Mission_id"

Diese Abfrage gibt also eine Liste von Agenten mit einer Mission aus der Datenbank an Active Record zurück, die wiederum Ruby-Objekte daraus erstellen soll. Das Mission Daten sind verfügbar, da die Daten aus diesen Zeilen mit den Zeilen der Agentendaten verbunden werden. Das heißt, die verbundenen Daten sind während der Abfrage verfügbar, werden jedoch nicht an Active Record zurückgegeben. So haben Sie diese Daten, um zum Beispiel Berechnungen durchzuführen. 

Das ist besonders cool, weil Sie Daten verwenden können, die nicht ebenfalls an Ihre App gesendet werden. Weniger Attribute, die in Ruby-Objekte integriert werden müssen und die Speicher beanspruchen, können ein großer Gewinn sein. Denken Sie im Allgemeinen daran, nur die absolut erforderlichen Zeilen und Spalten zurückzuschicken, die Sie benötigen. Auf diese Weise können Sie ein Aufblähen ganz vermeiden.

Schienen

Agent.all.joins (: mission) .where (Missionen: Ziel: "Die Welt retten")

Nur kurz zur Syntax hier: weil wir das nicht abfragen Agent Tisch über woher, aber die haben sich angeschlossen :Mission Tabelle müssen wir angeben, dass wir nach bestimmten suchen Missionen in unserer WOHER Klausel.

SQL

SELECT "agents". * FROM "agents" INNER JOIN "-Missionen" ON "-Missionen". "Id" = "Agenten". "Mission_id" WO "-Missionen". "Ziel" =? [["objektiv", "die welt retten"]]

Verwenden beinhaltet Hier würden auch Missionen an Active Record zurückgegeben, um das Laden von Ruby-Objekten zu erleichtern.

Verschmelzen

EIN verschmelzen Das ist beispielsweise praktisch, wenn Sie eine Abfrage für Agenten und die zugehörigen Missionen mit einem von Ihnen definierten spezifischen Gültigkeitsbereich kombinieren möchten. Wir können zwei nehmen ActiveRecord :: Relation Objekte und verschmelzen ihre Bedingungen. Sicher, kein Biggie, aber verschmelzen ist nützlich, wenn Sie einen bestimmten Bereich verwenden möchten, während Sie eine Zuordnung verwenden. 

Mit anderen Worten, was können wir damit machen? verschmelzen wird nach einem benannten Bereich im verbundenen Modell gefiltert. In einem der vorherigen Beispiele haben wir Klassenmethoden verwendet, um solche genannten Bereiche selbst zu definieren.

Schienen

Klasse Mission < ActiveRecord::Base has_many :agents def self.dangerous where(enemy: "Ernst Stavro Blofeld") end end class Agent < ActiveRecord::Base belongs_to :mission end

Schienen

Agent.joins (: mission) .merge (Mission.dangerous)

SQL

SELECT "agents". * FROM "agents" INNER JOIN "-Missionen" ON "-Missionen". "Id" = "Agenten". "Mission_id" WO "-Missionen". "Feind" =? [["Feind", "Ernst Stavro Blofeld"]]

Wenn wir was einfassen gefährlich Mission ist innerhalb der Mission Modell, wir können es auf ein verstauen Beitreten über verschmelzen diesen Weg. Die Logik dieser Bedingungen auf das relevante Modell zu verschieben, auf das sie gehört, ist auf der einen Seite eine nette Technik, um eine lockerere Kopplung zu erreichen. Wir möchten nicht, dass unsere Active Record-Modelle viele Details über einander und über die anderen wissen Andererseits gibt es Ihnen eine schöne API in Ihren Joins, ohne in Ihr Gesicht zu springen. Das folgende Beispiel ohne Zusammenführen würde ohne Fehler nicht funktionieren:

Schienen

Agent.all.merge (Mission.dangerous)

SQL

SELECT "Agenten". * FROM "Agenten" WO "Missionen". "Feind" =? [["Feind", "Ernst Stavro Blofeld"]]

Wenn wir jetzt ein ActiveRecord :: Relation Objekt für unsere Missionen an unsere Agenten, die Datenbank weiß nicht, über welche Missionen wir sprechen. Wir müssen klarstellen, welche Assoziation wir benötigen, und die Missionsdaten zuerst verbinden - oder SQL wird verwirrt. Eine letzte Kirsche an der Spitze. Wir können dies noch besser verkapseln, indem wir auch die Agenten einbeziehen: 

Schienen

Klasse 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

Schienen

Agent.double_o_engagements

SQL

SELECT "agents". * FROM "agents" INNER JOIN "-Missionen" ON "-Missionen". "Id" = "Agenten". "Mission_id" WO "-Missionen". "Feind" =? [["Feind", "Ernst Stavro Blofeld"]]

Das ist etwas Süßes in meinem Buch. Kapselung, richtige OOP und gute Lesbarkeit. Jackpot!

hat viele

Oben haben wir die gesehen gehört Vereinigung in Aktion viel. Lassen Sie uns dies aus einer anderen Perspektive betrachten und die Geheimdienstabschnitte in die Mischung einbringen:

Schienen

Klasse Abschnitt < 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

Agenten hätten also in diesem Szenario nicht nur eine mission_id aber auch a section_id. So weit, ist es gut. Lassen Sie uns alle Abschnitte mit Agenten finden, die eine bestimmte Mission haben, also eine Art Mission.

Schienen

Section.joins (: Agenten)

SQL

SELECT "Abschnitte". * FROM "Abschnitte" INNER JOIN "Agenten" ON "Agenten". "Section_id" = "Abschnitte." ID "

Hast du 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 folgt: "agents". "section_id" = "Abschnitte." id ". Mit anderen Worten, wir suchen nach einem Fremdschlüssel aus einer Tabelle, der wir beitreten.

Schienen

Agent.Joins (: Mission)

SQL

SELECT "agents". * FROM "agents" INNER JOIN "-Missionen" ON "-Missionen". "Id" = "Agenten". "Mission_id"

Zuvor waren unsere Joins über eine gehört Verband sah so aus: Die Fremdschlüssel wurden gespiegelt ("Missionen". "ID" = "Agenten". "Mission_id") und suchten nach dem Fremdschlüssel aus der Tabelle, die wir starten.

Geh zurück zu deinem hat viele In diesem Szenario würden wir jetzt eine Liste von Abschnitten erhalten, die wiederholt werden, da sie natürlich in jedem Abschnitt mehrere Agenten haben. Daher erhalten wir für jede Agentenspalte, für die eine Verknüpfung erstellt wird, eine Zeile für diesen Abschnitt oder kurz Abschnitts-ID. In diesem Fall werden Zeilen im Wesentlichen dupliziert. Um dies noch schwindeliger zu machen, lassen Sie uns auch Missionen in die Mischung einfließen lassen.

Schienen

Section.joins (Agenten:: Mission)

SQL

SELECT "Abschnitte". * FROM "Abschnitte" INNER JOIN "Agenten" ON "Agenten". "Section_id" = "Abschnitte". "ID" INNER JOIN "Missionen" ON "Missionen". "ID" = "Agenten". " mission_id "

Schauen Sie sich die beiden an INNER JOIN Teile. Immer noch bei mir? Wir „erreichen“ über Agenten ihre Missionen aus der Abteilung des Agenten. Ja, Zeug für lustige Kopfschmerzen, ich weiß. Was wir bekommen, sind Missionen, die indirekt mit einem bestimmten Abschnitt verbunden sind. 

Als Ergebnis erhalten wir neue Spalten, die zusammengefügt werden, aber die Anzahl der Zeilen ist immer noch dieselbe, die von dieser Abfrage zurückgegeben wird. Was an Active Record zurückgesendet wird, wodurch neue Ruby-Objekte erstellt werden, ist auch weiterhin die Liste der Abschnitte. Wenn also mehrere Missionen mit mehreren Agenten ausgeführt werden, würden wir erneut doppelte Zeilen für unseren Abschnitt erhalten. Lassen Sie uns das noch etwas filtern:

Schienen

Section.joins (Agenten:: Mission) .where (Missionen: Feind: "Ernst Stavro Blofeld") 

SQL

SELECT "Abschnitte". * FROM "Abschnitte" INNER JOIN "Agenten" ON "Agenten". "Section_id" = "Abschnitte". "ID" INNER JOIN "Missionen" ON "Missionen". "ID" = "Agenten". " mission_id "WO" -Missionen "." Feind "= 'Ernst Stavro Blofeld'

Jetzt bekommen wir nur Abschnitte zurück, die an Missionen beteiligt sind, bei denen Ernst Stavro Blofeld der betroffene Feind ist. Kosmopolitisch, wie manche Superschurken von sich selbst denken könnten, könnten sie in mehr als einem Abschnitt operieren, etwa in den Abschnitten A und C, den Vereinigten Staaten und Kanada. 

Wenn sich in einem bestimmten Bereich mehrere Agenten befinden, die an der gleichen Mission arbeiten, um Blofeld oder was auch immer zu stoppen, hätten wir uns erneut in Active Record um Reihen gewehrt. Lassen Sie uns etwas deutlicher sein:

Schienen

Section.joins (Agenten:: Mission) .where (Missionen: Feind: "Ernst Stavro Blofeld")

SQL

SELECT DISTINCT "Abschnitte". * FROM "Abschnitte" INNER JOIN "Agenten" ON "Agenten". "Section_id" = "Abschnitte". "ID" INNER JOIN "Missionen" ON "Missionen". "ID" = "Agenten". "mission_id" WO "Missionen". "Feind" = 'Ernst Stavro Blofeld'

Was uns dies gibt, ist die Anzahl der Abschnitte, aus denen Blofeld arbeitet - die bekannt sind -, die Agenten in Missionen mit ihm als Feind betreibt. Als letzten Schritt wollen wir noch einmal etwas überarbeiten. Wir extrahieren dies in eine nette "kleine" Klassenmethode Klasse Abschnitt:

Schienen

Klasse Abschnitt < 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 mehr umgestalten und die Verantwortlichkeiten aufteilen, um eine lockerere Kopplung zu erreichen, aber machen wir jetzt weiter.

Benutzerdefinierte Joins 

In den meisten Fällen können Sie sich darauf verlassen, dass Active Record die von Ihnen gewünschte SQL schreibt. Das bedeutet, dass Sie sich in Ruby Land aufhalten und sich nicht zu viele Details über die Datenbank machen müssen. Aber manchmal müssen Sie ein Loch in SQL landen und Ihr eigenes Ding machen. Zum Beispiel, wenn Sie eine LINKS Schließen Sie sich dem üblichen Verhalten von Active Record an und brechen Sie aus INNERE standardmäßig beitreten. schließt sich an ist ein kleines Fenster, um bei Bedarf eigene SQL zu schreiben. 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 an einem Beispiel demonstrieren, das Gadgets beinhaltet. Sagen wir normalerweise einen typischen Agenten hat viele Gadgets, und wir möchten Agenten finden, die nicht mit ausgefallenen Geräten ausgestattet sind, um ihnen auf dem Feld zu helfen. Ein gewöhnlicher Join würde keine guten Ergebnisse liefern, da wir eigentlich daran interessiert sind Null-oder Null in SQL sprechen Werte dieser Spionspielzeuge.

Schienen

Klasse 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 machen schließt sich an Betrieb erhalten wir nur Agenten zurück, die bereits mit Gadgets ausgestattet sind, weil die agent_id auf diesen Geräten ist nicht gleich Null. Dies ist das erwartete Verhalten eines standardmäßigen inneren 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 einem Null Der Wert für einen Agenten, der kein Gadget enthält, entspricht diesem Kriterium nicht. 

Schienen

Agent.joins (: Gadgets)

SQL

SELECT "agents". * FROM "agents" INNER JOIN "Gadgets" ON "Gadgets". "Agent_id" = "agents". "Id"

Auf der anderen Seite suchen wir nach schmucken Agenten, die dringend etwas Liebe vom Quartiermeister brauchen. Ihre erste Vermutung könnte wie folgt aussehen:

Schienen

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" IST NULL

Nicht schlecht, aber wie Sie an der SQL-Ausgabe sehen können, wird sie nicht mitgespielt und besteht trotzdem auf der Standardeinstellung INNER JOIN. In diesem Szenario brauchen wir eine ÄUSSERE beitreten, weil eine Seite unserer "Gleichung" sozusagen fehlt. Wir suchen nach Ergebnissen für Gadgets, die nicht existieren - genauer gesagt, für Agenten ohne Gadgets. 

Als wir bisher in einem Join ein Symbol an Active Record übergeben haben, erwartete es eine Assoziation. Bei einer übergebenen Zeichenfolge erwartet sie dagegen, dass es sich um ein tatsächliches Fragment des SQL-Codes handelt - ein Teil Ihrer Abfrage.

Schienen

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" ist NULL

Oder wenn Sie neugierig auf faule Agenten sind, die keine Missionen haben - möglicherweise auf Barbados oder wo auch immer -, würde unser Custom-Join so aussehen:

Schienen

Agent.joins ("LEFT OUTER JOIN-Missionen auf Missionen.id = agents.mission_id"). Where (Missionen: id: nil)

SQL

SELECT "agents". * FROM "agents" LEFT OUTER JOIN-Missionen auf missions.id = agents.mission_id WHERE "missions". "Id" ist NULL

Der äußere Join ist die umfassendere Join-Version, da sie mit allen Datensätzen der verbundenen Tabellen übereinstimmt, auch wenn einige dieser Beziehungen noch nicht vorhanden sind. Da diese Herangehensweise nicht so exklusiv ist wie innere Verbindungen, werden Sie hier und da eine Menge Nils bekommen. Dies kann in manchen Fällen natürlich informativ sein, aber innere Verbindungen sind in der Regel das, wonach wir suchen. Mit Rails 5 können wir eine spezialisierte Methode verwenden left_outer_joins stattdessen für solche Fälle. Endlich! 

Eine Kleinigkeit für die Straße: Halten Sie diese Löcher so weit wie möglich in SQL-Land, wenn Sie können. Sie werden allen, einschließlich Ihres zukünftigen Selbst, einen enormen Gefallen tun.

Abschließende Gedanken

Active Record für das Schreiben von effizientem SQL zu erhalten, ist eine der wichtigsten Fähigkeiten, die Sie aus dieser Mini-Serie für Anfänger mitnehmen sollten. Auf diese Weise erhalten Sie auch Code, der mit der unterstützten Datenbank kompatibel ist. Dies bedeutet, dass die Abfragen über Datenbanken hinweg stabil sind. Es ist wichtig, dass Sie nicht nur verstehen, wie Sie mit Active Record spielen, sondern auch die zugrunde liegende SQL, die von gleicher Bedeutung ist. 

Ja, SQL kann langweilig sein, langweilig zu lesen und nicht elegant aussehen, aber vergessen Sie nicht, dass Rails Active Record um SQL herumwickelt, und Sie sollten es nicht vernachlässigen, dieses wichtige Stück Technologie zu verstehen - nur weil Rails es sehr leicht macht, sich nicht darum zu kümmern der ganzen Zeit. Effizienz ist für Datenbankabfragen von entscheidender Bedeutung, insbesondere wenn Sie etwas für größere Zielgruppen mit hohem Datenverkehr erstellen. 

Gehen Sie jetzt auf die Internets und finden Sie mehr Material zu SQL, um es ein für alle Mal aus Ihrem System zu entfernen!