Dans ce dernier article, nous allons examiner un peu plus en profondeur les requêtes et jouer avec quelques scénarios plus avancés. Nous traiterons un peu plus des relations des modèles Active Record dans cet article, mais je m'éloignerai des exemples qui pourraient être trop déroutants pour la programmation des débutants. Avant de continuer, des exemples tels que l'exemple ci-dessous ne doivent pas créer de confusion:
Mission.last.agents.where (nom: 'James Bond')
Si vous êtes nouveau dans les requêtes Active Record et SQL, je vous recommande de consulter mes deux articles précédents avant de continuer. Celui-ci pourrait être difficile à avaler sans la connaissance que je construisais jusqu'à présent. À vous, bien sûr. D'un autre côté, cet article ne sera pas aussi long que les autres si vous voulez simplement regarder ces cas d'utilisation légèrement avancés. Creusons!
Réitérons. Nous pouvons interroger les modèles Active Record tout de suite, mais les associations sont également un jeu équitable pour les requêtes et nous pouvons chaîner tous ces trucs. Jusqu'ici tout va bien. Nous pouvons également intégrer dans vos modèles des viseurs propres et réutilisables, et j'ai brièvement évoqué leur similitude avec les méthodes de classe..
agent de classe < ActiveRecord::Base belongs_to :mission scope :find_bond, -> where (name: 'James Bond') portée: licenced_to_kill, -> where (licence_to_kill: true) portée: womanizer, -> where (womanizer: true) portée: joueur, -> where (joueur: 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
Ainsi, vous pouvez également les intégrer à vos propres méthodes de classe et en finir. Les scopes ne sont pas louches ou quoi que ce soit, je pense - bien que les gens les mentionnent comme étant un peu magiques ici et là - mais puisque les méthodes de classe permettent d'obtenir la même chose, j'opterais pour cela..
agent de classe < 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
Ces méthodes de classe se lisent exactement de la même manière, et vous n'avez pas besoin de poignarder quelqu'un avec un lambda. Tout ce qui fonctionne le mieux pour vous ou votre équipe. c'est à vous de choisir quelle API vous souhaitez utiliser. Il suffit de ne pas les mélanger et les assortir: un seul choix! Les deux versions vous permettent de chaîner facilement ces méthodes dans une autre méthode de classe, par exemple:
agent de classe < 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.agens.find_licenced.htind
agent de classe < 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
Allons un peu plus loin, reste avec moi. Nous pouvons utiliser un lambda dans les associations elles-mêmes pour définir une certaine portée. Cela semble un peu bizarre au début, mais ils peuvent être très utiles. Cela permet d’appeler ces lambdas directement sur vos associations.
C'est assez cool et très lisible avec des méthodes plus courtes d'enchaînement. Attention aux couplages trop serrés entre ces modèles.
classe Mission < ActiveRecord::Base has_many :double_o_agents, -> where (licence_to_kill: true), nom_classe: "Agent" end # =>> Mission.double_o_agents
Dis-moi que ce n'est pas cool en quelque sorte! Ce n'est pas pour un usage quotidien, mais dope d'avoir je suppose. Alors voici Mission
peut "demander" uniquement les agents qui ont la licence pour tuer.
Un mot sur la syntaxe, puisque nous nous sommes écartés des conventions de nommage et avons utilisé quelque chose de plus expressif, comme double_o_agents
. Nous devons mentionner le nom de la classe afin de ne pas confondre Rails, qui pourrait sinon s’attendre à chercher une classe. DoubleOAgent
. Vous pouvez bien sûr avoir les deux Agent
les associations en place, comme d'habitude et votre personnalisé, et Rails ne se plaindront pas.
classe Mission < ActiveRecord::Base has_many :agents has_many :double_o__agents, -> where (licence_to_kill: true), nom_classe: "Agent" end # => Mission.agents # => Mission.double_o_agents
Lorsque vous interrogez la base de données pour des enregistrements et que vous n'avez pas besoin de toutes les données, vous pouvez choisir de spécifier ce que vous voulez exactement renvoyer. Pourquoi? Les données renvoyées à Active Record seront éventuellement intégrées à de nouveaux objets Ruby. Examinons une stratégie simple pour éviter la surcharge de mémoire dans votre application Rails:
classe Mission < ActiveRecord::Base has_many :agents end class Agent < ActiveRecord::Base belongs_to :mission end
Agent.all.joins (: mission)
SELECTIONNEZ "agents". * FROM "agents" INNER JOIN "missions" ON "missions". "Id" = "agents". "Mission_id"
Cette requête renvoie donc une liste d'agents ayant une mission de la base de données dans Active Record, qui à son tour tente de créer des objets Ruby à partir de celle-ci. le mission
les données sont disponibles puisque les données de ces lignes sont jointes aux lignes des données de l'agent. Cela signifie que les données jointes sont disponibles pendant la requête mais ne seront pas renvoyées à Active Record. Donc, vous aurez ces données pour effectuer des calculs, par exemple.
C'est particulièrement intéressant, car vous pouvez utiliser des données qui ne sont pas également renvoyées vers votre application. Moins d'attributs qui doivent être intégrés aux objets Ruby (qui utilisent de la mémoire) peuvent être très rentables. En général, pensez à ne renvoyer que les lignes et les colonnes absolument nécessaires dont vous avez besoin. De cette façon, vous pouvez éviter un peu de gonflement.
Agent.all.joins (: mission) .where (missions: objectif: "Sauver le monde")
Juste un bref commentaire sur la syntaxe: parce que nous ne demandons pas la Agent
tableau via où
, mais le joint :mission
table, nous devons spécifier que nous recherchons des missions
dans notre OÙ
clause.
SELECTIONNER "agents". * FROM "agents" INNER JOIN "missions" ON "missions". "Id" = "agents". "Mission_id" WHERE "missions". "Objectif" =? [["objectif", "Sauver le monde"]]
En utilisant comprend
Ici aussi, les missions rendues à Active Record pour un chargement rapide et utiliseraient des objets Ruby pour créer de la mémoire..
UNE fusionner
est pratique, par exemple, lorsque nous souhaitons combiner une requête sur des agents et leurs missions associées ayant une étendue spécifique que vous définissez. On peut en prendre deux ActiveRecord :: Relation
objets et fusionner leurs conditions. Bien sûr, pas trop grave, mais fusionner
est utile si vous souhaitez utiliser une certaine portée lors de l'utilisation d'une association.
En d'autres termes, ce que nous pouvons faire avec fusionner
est filtré par une portée nommée sur le modèle joint. Dans l’un des exemples précédents, nous avons utilisé des méthodes de classe pour définir nous-mêmes ces portées nommées..
classe Mission < ActiveRecord::Base has_many :agents def self.dangerous where(enemy: "Ernst Stavro Blofeld") end end class Agent < ActiveRecord::Base belongs_to :mission end
Agent.joins (: mission) .merge (Mission.dangerous)
SÉLECTIONNER "agents". * FROM "agents" INNER JOIN "missions" ON "missions". "Id" = "agents". "Mission_id" WHERE "missions". "Ennemi" =? [["ennemi", "Ernst Stavro Blofeld"]]
Quand on encapsule quel dangereux
la mission est dans le Mission
modèle, nous pouvons le glisser sur un joindre
via fusionner
par ici. Ainsi, déplacer la logique de telles conditions vers le modèle pertinent auquel il appartient est d'un côté une technique agréable pour obtenir un couplage plus souple. Nous ne voulons pas que nos modèles Active Record connaissent beaucoup de détails les uns sur les autres, et de l'autre. Par contre, il vous donne une belle API dans vos jointures sans exploser à la figure. L'exemple ci-dessous sans fusion ne fonctionnerait pas sans erreur:
Agent.all.merge (Mission.dangerous)
SÉLECTIONNER "agents". * FROM "agents" O "missions". "Ennemi" =? [["ennemi", "Ernst Stavro Blofeld"]]
Quand on fusionne maintenant un ActiveRecord :: Relation
objet pour nos missions sur nos agents, la base de données ne sait pas de quelles missions nous parlons. Nous devons définir clairement l'association dont nous avons besoin et rejoindre les données de la mission en premier, sinon SQL devient confus. Une dernière cerise sur le dessus. Nous pouvons encore mieux encapsuler ceci en impliquant également les agents:
classe 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
Agent.double_o_engagements
SÉLECTIONNER "agents". * FROM "agents" INNER JOIN "missions" ON "missions". "Id" = "agents". "Mission_id" WHERE "missions". "Ennemi" =? [["ennemi", "Ernst Stavro Blofeld"]]
C'est une cerise dans mon livre. Encapsulation, POO appropriée et grande lisibilité. Cagnotte!
Ci-dessus, nous avons vu le appartient à
association en action beaucoup. Examinons cela sous un autre angle et intégrons des sections de services secrets:
Section de classe < 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
Ainsi, dans ce scénario, les agents auraient non seulement un mission_id
mais aussi un section_id
. Jusqu'ici tout va bien. Trouvons toutes les sections avec des agents ayant une mission spécifique, donc des sections ayant une sorte de mission en cours..
Section.joins (: agents)
SELECT "sections". * FROM "sections" INNER JOIN "agents" ON "agents". "Section_id" = "sections." Id "
Avez-vous remarqué quelque chose? Un petit détail est différent. Les clés étrangères sont retournées. Ici, nous demandons une liste de sections mais utilisons des clés étrangères comme ceci: "agents". "section_id" = "sections." id "
. En d'autres termes, nous recherchons une clé étrangère à partir d'une table à laquelle nous rejoignons.
Agent.joins (: mission)
SELECTIONNEZ "agents". * FROM "agents" INNER JOIN "missions" ON "missions". "Id" = "agents". "Mission_id"
Auparavant, nos jointures via un appartient à
L’association ressemblait à ceci: les clés étrangères se reflétaient ("missions". "id" = "agents". "mission_id"
) et recherchions la clé étrangère de la table que nous commençons.
Revenir à votre a beaucoup
scénario, nous aurions maintenant une liste de sections qui sont répétées car ils ont plusieurs agents dans chaque section, bien sûr. Ainsi, pour chaque colonne d'agent qui est jointe, nous obtenons une ligne pour cette section ou section_id-in, nous dupliquons essentiellement les lignes. Pour rendre cela encore plus vertigineux, ajoutons également des missions..
Section.joins (agents:: mission)
SELECT "sections". * FROM "sections" INNER JOIN "agents" ON "agents". "Section_id" = "sections". "Id" INNER JOIN "missions" ON "missions". "Id" = "agents". " mission_id "
Découvrez les deux JOINTURE INTERNE
les pièces. Encore avec moi? Nous "atteignons" par l'intermédiaire des agents leurs missions à partir de leur section. Oui, des trucs pour des maux de tête amusants, je sais. Ce que nous obtenons sont des missions associées indirectement à une certaine section.
En conséquence, nous obtenons de nouvelles colonnes jointes, mais le nombre de lignes est toujours le même que celui renvoyé par cette requête. Ce qui est renvoyé à Active Record - entraînant la construction de nouveaux objets Ruby - reste également la liste des sections. Ainsi, lorsque nous avons plusieurs missions avec plusieurs agents, nous obtenons à nouveau des lignes dupliquées pour notre section. Filtrons ceci un peu plus:
Section.joins (agents:: mission) .where (missions: ennemi: "Ernst Stavro Blofeld")
SELECT "sections". * FROM "sections" INNER JOIN "agents" ON "agents". "Section_id" = "sections". "Id" INNER JOIN "missions" ON "missions". "Id" = "agents". " mission_id "WHERE" missions "." ennemis "= 'Ernst Stavro Blofeld'
Nous ne retrouvons plus que les sections impliquées dans des missions où Ernst Stavro Blofeld est l'ennemi impliqué. Cosmopolitan comme certains super méchants pourraient penser à eux-mêmes, ils pourraient opérer dans plus d'une section, disons les sections A et C, les États-Unis et le Canada respectivement.
Si nous avons plusieurs agents dans une section donnée qui travaillent sur la même mission pour arrêter Blofeld ou autre, nous aurions de nouveau des lignes répétées retournées dans Active Record. Soyons un peu plus distincts à ce sujet:
Section.joins (agents:: mission) .where (missions: ennemi: "Ernst Stavro Blofeld"). Distinct
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". "ennemis" = 'Ernst Stavro Blofeld'
Cela nous donne le nombre de sections sur lesquelles Blofeld opère, qui sont connues, et qui ont des agents actifs dans des missions avec lui comme ennemi. Enfin, refactorisons-nous. Nous extrayons cela dans une belle "petite" méthode de classe sur Section de classe
:
Section de classe < 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
Vous pouvez le reformuler encore plus et diviser les responsabilités pour obtenir un couplage plus souple, mais passons à autre chose pour l'instant..
La plupart du temps, vous pouvez compter sur Active Record pour écrire le code SQL que vous souhaitez pour vous. Cela signifie que vous restez au pays Ruby et que vous n'avez pas à vous soucier des détails de la base de données. Mais parfois, vous devez faire un trou dans SQL et faire votre propre chose. Par exemple, si vous devez utiliser un LA GAUCHE
rejoindre et sortir du comportement habituel de Active Record de faire une INTERNE
rejoindre par défaut. se joint
est une petite fenêtre pour écrire votre propre code SQL personnalisé si nécessaire. Vous l'ouvrez, branchez votre code de requête personnalisé, fermez la “fenêtre” et pouvez continuer à ajouter des méthodes de requête Active Record.
Montrons ceci avec un exemple impliquant des gadgets. Disons un agent typique habituellement a beaucoup
gadgets, et nous souhaitons trouver des agents qui ne disposent pas de gadgets sophistiqués pour les aider sur le terrain. Une jointure habituelle ne donnerait pas de bons résultats puisque nous sommes réellement intéressés par néant
-ou nul
en SQL parle valeurs de ces jouets espion.
classe 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
Quand on fait un se joint
opération, nous ne récupérerons que les agents déjà équipés de gadgets, car le agent_id
sur ces gadgets n'est pas nul. C'est le comportement attendu d'une jointure interne par défaut. La jointure interne repose sur une correspondance des deux côtés et renvoie uniquement les lignes de données correspondant à cette condition. Un gadget inexistant avec un néant
valeur pour un agent qui ne porte aucun gadget ne correspond pas à ce critère.
Agent.joins (: gadgets)
SELECT "agents". * FROM "agents" INNER JOIN "gadgets" ON "gadgets". "Agent_id" = "agents". "Id"
D'autre part, nous recherchons des agents dégueulasses qui ont désespérément besoin de l'amour du quartier-maître. Votre première hypothèse pourrait ressembler à ceci:
Agent.joins (: gadgets) .where (gadgets: agent_id: nil)
SELECTIONNEZ "agents". * FROM "agents" INNER JOIN "gadgets" ON "gadgets". "Agent_id" = "agents". "Id" WHERE "gadgets". "Agent_id" IS NULL
Pas mal, mais comme vous pouvez le voir à la sortie SQL, il ne joue pas et insiste toujours sur la valeur par défaut JOINTURE INTERNE
. C'est un scénario où nous avons besoin d'un EXTÉRIEUR
rejoindre, car il manque un côté de notre «équation», pour ainsi dire. Nous recherchons des résultats pour des gadgets qui n'existent pas, plus précisément, pour des agents sans gadgets..
Jusqu'à présent, lorsque nous avons passé un symbole à Active Record dans une jointure, il s'attendait à une association. Avec une chaîne transmise, d'un autre côté, il s'attend à ce qu'il s'agisse d'un fragment de code SQL, une partie de votre requête..
Agent.joins ("gadgets LEFT OUTER JOIN ON gadgets.agent_id = agents.id"). Where (gadgets: agent_id: nil)
SÉLECTIONNEZ "agents". * FROM "agents" gadgets LEFT OUTER JOIN ON gadgets.agent_id = agents.id O "" gadgets "." Agent_id "EST NULL
Ou, si vous êtes intéressé par des agents paresseux sans mission suspendue, éventuellement à la Barbade ou ailleurs, notre adhésion personnalisée ressemblerait à ceci:
Agent.joins ("missions LEFT OUTER JOIN ON sur missions.id = agents.mission_id"). Où (missions: id: nil)
SELECT "agents". * FROM "agents" missions LEFT OUTER JOIN ON missions.id = agents.mission_id O_ "missions". "Id" EST NULL
La jointure externe est la version de jointure la plus inclusive, car elle correspond à tous les enregistrements des tables jointes, même si certaines de ces relations n'existent pas encore. Parce que cette approche n'est pas aussi exclusive que les jointures internes, vous obtiendrez un tas de nil ici et là. Bien sûr, cela peut être informatif dans certains cas, mais les jointures internes sont néanmoins ce que nous recherchons. Rails 5 nous laissera utiliser une méthode spécialisée appelée left_outer_joins
au lieu de cela pour de tels cas. finalement!
Une petite chose pour la route: gardez ces trous aussi minimes que possible dans la zone SQL, si vous le pouvez. Vous ferez tout le monde, y compris votre avenir, une immense faveur.
Obtenir Active Record pour écrire du SQL efficace pour vous est l’une des principales compétences que vous devriez acquérir dans cette mini-série pour les débutants. De cette façon, vous obtiendrez également un code compatible avec la base de données prise en charge, ce qui signifie que les requêtes seront stables entre les bases de données. Il est nécessaire que vous compreniez non seulement comment utiliser Active Record, mais également le code SQL sous-jacent, qui est tout aussi important..
Oui, SQL peut être ennuyeux, fastidieux à lire et pas élégant, mais n'oubliez pas que Rails encapsule Active Record autour de SQL et que vous ne devez pas négliger de comprendre cette technologie essentielle, juste parce que Rails vous permet de ne pas trop vous soucier de rien du temps. L’efficacité est cruciale pour les requêtes de base de données, en particulier si vous créez quelque chose pour un public plus large avec un trafic important.
Allez maintenant sur les internets et trouvez du matériel supplémentaire sur SQL pour le sortir de votre système, une fois pour toutes.!