Requêtes dans Rails, Partie 2

Dans ce deuxième article, nous allons plonger un peu plus loin dans les requêtes Active Record dans Rails. Si vous êtes encore novice en SQL, je vais ajouter des exemples assez simples pour que vous puissiez les suivre et en extraire un peu la syntaxe au fur et à mesure.. 

Cela étant dit, il serait certainement utile de parcourir un didacticiel SQL rapide avant de continuer à lire. Sinon, prenez votre temps pour comprendre les requêtes SQL que nous avons utilisées, et j'espère que d'ici la fin de cette série, cela ne sera plus intimidant. 

La plupart des choses sont vraiment simples, mais la syntaxe est un peu bizarre si vous débutez avec le codage, en particulier en Ruby. Accrochez-vous, ce n'est pas sorcier!

Les sujets

  • Comprend & Eager Loading
  • Tables de jonction
  • Chargement désagréable
  • Portées
  • Agrégations
  • Chercheurs dynamiques
  • Champs spécifiques
  • SQL personnalisé

Comprend & Eager Loading

Ces requêtes incluent plusieurs tables de base de données sur lesquelles travailler et pourraient être les plus importantes à retirer de cet article. Cela se résume à ceci: au lieu de faire plusieurs requêtes pour des informations réparties sur plusieurs tables, comprend essaie de les garder au minimum. Le concept clé de cette opération s'appelle «chargement anticipé» et signifie que nous chargeons les objets associés lorsque nous effectuons une recherche..

Si nous le faisions en itérant sur une collection d'objets, puis en essayant d'accéder à ses enregistrements associés depuis une autre table, nous rencontrerions un problème appelé «problème de requête N + 1». Par exemple, pour chaque agent.handler dans une collection d'agents, nous lançons des requêtes distinctes pour les agents et leurs gestionnaires. C’est ce que nous devons éviter, car cela ne s’étend pas du tout. Au lieu de cela, nous faisons ce qui suit:

Des rails

agents = Agent.includes (: gestionnaires)

Si nous parcourons maintenant une telle collection d'agents (remise en compte), nous ne serons pas limités au nombre d'enregistrements retournés pour le moment; nous aurons deux requêtes au lieu d'un gazillion.. 

SQL

SELECT "agents". * FROM "agents" SELECT "gestionnaires". * FROM "gestionnaires" WHERE "gestionnaires". "Id" IN (1, 2)

Cet agent de la liste a deux gestionnaires et lorsque nous demandons maintenant à l'agent agent de lui attribuer ses gestionnaires, aucune requête de base de données supplémentaire ne doit être déclenchée. Nous pouvons bien sûr aller plus loin et charger avec impatience plusieurs enregistrements de table associés. Si nous devions charger non seulement des gestionnaires mais également les missions associées de l'agent pour une raison quelconque, nous pourrions utiliser comprend comme ça.

Des rails

agents = Agent.includes (: gestionnaires,: mission)

Simple! Faites attention à l’utilisation des versions singulière et plurielle pour les includes. Ils dépendent de vos associations de modèles. UNE a beaucoup association utilise le pluriel, alors qu'un appartient à ou un en a un a besoin de la version singulière, bien sûr. Si vous avez besoin, vous pouvez aussi ranger un clause permettant de spécifier des conditions supplémentaires, mais la méthode recommandée pour spécifier les conditions des tables associées chargées avec impatience consiste à utiliser se joint au lieu. 

Une chose à garder à l'esprit sur le chargement rapide est que les données qui seront ajoutées seront renvoyées intégralement à Active Record, qui générera à son tour des objets Ruby incluant ces attributs. Cela contraste avec la «simple» jonction des données, où vous obtiendrez un résultat virtuel que vous pourrez utiliser pour des calculs, par exemple, et épuisera moins votre mémoire que celle-ci..

Tables de jonction

La jonction de tables est un autre outil qui vous permet d’éviter d’envoyer trop de requêtes inutiles dans le pipeline. Un scénario courant consiste à joindre deux tables avec une seule requête qui renvoie une sorte d’enregistrement combiné.. se joint est juste une autre méthode de recherche d’Enregistrements actifs qui vous permet d’être en termes SQL-JOINDRE les tables. Ces requêtes peuvent renvoyer des enregistrements combinés à partir de plusieurs tables et vous obtenez une table virtuelle qui combine les enregistrements de ces tables. C'est assez radieux quand vous comparez cela à des requêtes de toutes sortes pour chaque table. Il existe plusieurs types de chevauchement de données que vous pouvez obtenir avec cette approche.. 

La jointure interne est le mode opératoire par défaut pour se joint. Cela correspond à tous les résultats correspondant à un certain identifiant et à sa représentation en tant que clé étrangère provenant d'un autre objet ou d'une autre table. Dans l’exemple ci-dessous, exprimez simplement: donnez-moi toutes les missions où identifiant se présente comme mission_id dans la table d'un agent. "agents". "mission_id" = "missions". "id". Les jointures internes excluent les relations qui n'existent pas.

Des rails

Mission.joins (: agents)

SQL

SÉLECTIONNEZ "missions". * FROM "missions" INNER JOIN "agents" ON "agents". "Mission_id" = "mission". "Id"

Nous faisons donc correspondre les missions et les agents qui les accompagnent en une seule requête! Bien sûr, nous pourrions commencer par obtenir les missions, les parcourir une par une et demander leurs agents. Mais nous reviendrions ensuite à notre redoutable «problème de requête N + 1». Non, merci! 

Ce qui est également bien avec cette approche, c'est que nous n'obtiendrons aucun cas nul avec des jointures internes; nous n'obtenons que les enregistrements renvoyés dont les identifiants correspondent aux clés étrangères des tables associées. Par exemple, s'il nous faut trouver des missions dépourvues d'agents, nous aurons plutôt besoin d'une jointure externe. Puisque cela implique actuellement d’écrire votre propre JOINT EXTÉRIEUR SQL, nous verrons cela dans le dernier article. De retour aux jointures standard, vous pouvez également joindre plusieurs tables associées, bien sûr..

Des rails

Mission.joins (: agents,: dépenses,: manutentionnaires)

Et vous pouvez ajouter sur ces quelques clauses pour spécifier vos trouveurs encore plus. Ci-dessous, nous ne recherchons que les missions exécutées par James Bond et uniquement les agents appartenant à la mission "Moonraker" du deuxième exemple..

Mission.joins (: agents) .where (agents: name: 'James Bond')

SQL

SÉLECTIONNER "missions". * FROM "missions" INNER JOIN "agents" ON "agents". "Mission_id" = "missions". "Id" WHERE "agents". "Name" =? [["nom", "James Bond"]]

Des rails

Agent.joins (: mission) .where (missions: mission_name: 'Moonraker')

SQL

SELECTIONNER "agents". * FROM "agents" INNER JOIN "missions" ON "missions". "Id" = "agents". "Id_mission" WHERE "missions". "Nom_mission" =? [["mission_name", "Moonraker"]]

Avec se joint, vous devez également faire attention à l'utilisation singulière et plurielle de vos associations modèles. Parce que mon Mission classe has_many: agents, on peut utiliser le pluriel. Par contre, pour le Agent classe appartient à la mission, seule la version singulière fonctionne sans exploser. Important petit détail: le la partie est plus simple. Puisque vous analysez plusieurs lignes de la table qui remplissent une certaine condition, la forme au pluriel a toujours un sens..

Portées

Les oscilloscopes constituent un moyen pratique d'extraire les besoins de requêtes courants en méthodes bien nommées. De cette façon, ils sont un peu plus faciles à comprendre et à comprendre si d’autres doivent travailler avec votre code ou si vous devez revoir certaines requêtes à l’avenir. Vous pouvez les définir pour des modèles uniques mais les utiliser également pour leurs associations.. 

Le ciel est la limite vraiment-se joint, comprend, et  sont tous jeu juste! Puisque les lunettes retournent aussi ActiveRecord :: Relations objets, vous pouvez les chaîner et appeler d’autres scopes dessus sans hésiter. Extraire de telles étendues et les chaîner pour des requêtes plus complexes est très pratique et rend les longues plus lisibles. Les portées sont définies via la syntaxe «stabby lambda»:

Des rails

classe Mission < ActiveRecord::Base has_many: agents scope :successful, -> where (mission_complete: true) end Mission.successful
agent de classe < ActiveRecord::Base belongs_to :mission scope :licenced_to_kill, -> where (licence_to_kill: true) portée: womanizer, -> où (womanizer: true) portée: joueur, -> where (joueur: true) end # Agent.gambler # Agent.womanizer # Agent.licenced_to_kill # Agent.womanizer.gambler Agent.licenced_to_kill.womanizer.gambler

SQL

SELECT "agents". * FROM "agents" WHERE "agents". "Licence_to_kill" =? ET "agents". "Womanizer" =? ET "agents". "Joueur" =? [["licence_to_kill", "t"], ["womanizer", "t"], ["joueur", "t"]]

Comme vous pouvez le constater dans l'exemple ci-dessus, trouver James Bond est bien plus agréable si vous pouvez simplement chaîner des étendues. De cette façon, vous pouvez mélanger et assortir diverses requêtes et rester au sec en même temps. Si vous avez besoin d'étendues via des associations, elles sont également à votre disposition:

Mission.last.agents.licenced_to_kill.womanizer.gambler
SELECT "missions". * FROM "missions" ORDER BY "missions". "Id" DESC LIMIT 1 SELECT "agents". * FROM "agents" WHERE "agents". "Mission_id" =? AND "agents". "Licence_to_kill" =? ET "agents". "Womanizer" =? ET "agents". "Joueur" =? [["mission_id", 33], ["licence_to_kill", "t"], ["womanizer", "t"], ["joueur", "t"]]

Vous pouvez également redéfinir le default_scope pour quand vous regardez quelque chose comme Mission.all.

classe Mission < ActiveRecord::Base default_scope  where status: "In progress"  end Mission.all

SQL

 SELECT "missions". * FROM "missions" O "missions". "Status" =? [["statut", "En cours"]]

Agrégations

Cette section n’est pas aussi avancée en termes de compréhension, mais vous en aurez besoin le plus souvent dans des scénarios pouvant être considérés comme un peu plus avancés que votre moyen chercheur. .tout, .premier, .find_by_id ou peu importe. Le filtrage basé sur des calculs de base, par exemple, est très probablement quelque chose que les débutants n'entrent pas en contact immédiatement. Que regardons-nous exactement ici?

  • somme
  • compter
  • le minimum
  • maximum
  • moyenne

Peasy facile, non? La chose intéressante est qu'au lieu de parcourir en boucle une collection d'objets renvoyée pour effectuer ces calculs, nous pouvons laisser Active Record faire tout ce travail pour nous et renvoyer ces résultats avec les requêtes, de préférence dans une requête. Gentil, hein?

  • compter

Des rails

Mission.count # => 24

SQL

SELECT COUNT (*) FROM "missions"
  • moyenne

Des rails

Agent.average (: number_of_gadgets) .to_f # => 3.5

SQL

SELECT AVG ("agents". "Number_of_gadgets") FROM "agents"

Puisque nous savons maintenant comment nous pouvons utiliser se joint, nous pouvons aller plus loin et ne demander que la moyenne des gadgets que les agents ont sur une mission particulière, par exemple.

Des rails

Agent.joins (: mission) .where (missions: name: 'Moonraker'). Average (: number_of_gadgets) .to_f # => 3.4

SQL

SELECT AVG ("agents". "Number_of_gadgets") FROM "agents" INNER JOIN "missions" ON "missions". "Id" = "agents". "Mission_id" WHERE "missions". "Name" =? [["nom", "Moonraker"]]

Le regroupement de ces gadgets en moyenne par nom de mission devient alors trivial. En savoir plus sur le regroupement ci-dessous:

Des rails

Agent.joins (: mission) .group ('missions.name'). Average (: number_of_gadgets)

SQL

SELECT AVG ("agents". "Number_of_gadgets") AS nombre_moyen_gadgets, missions.nom AS nom_missions FROM "agents" INNER JOIN "missions" ON "missions". "Id" = "agents". "Id_mission" GROUP BY missions.name
  • somme

Des rails

Agent.sum (: number_of_gadgets) Agent.where (licence_to_kill: true) .sum (: number_of_gadgets) Agent.where.not (licence_to_kill: true) .sum (: number_of_gadgets)

SQL

SELECT SUM ("agents". "Number_of_gadgets") FROM "agents" SELECT SUM ("agents". "Number_of_gadgets") FROM "agents" WHERE "agents". "Licence_to_kill" =? [["licence_to_kill", "t"]] SELECT SUM ("agents". "numéro_de_gadgets") FROM "agents" WHERE ("agents". "licence_to_kill"! =?) [["licence_to_kill", "t"]]
  • maximum

Des rails

Agent.maximum (: number_of_gadgets) Agent.where (licence_to_kill: true) .maximum (: number_of_gadgets) 

SQL

SELECT MAX ("agents". "Number_of_gadgets") FROM "agents" SELECT MAX ("agents". "Number_of_gadgets") FROM "agents" WHERE "agents". "Licence_to_kill" =? [["licence_to_kill", "t"]]
  • le minimum

Des rails

Agent.minimum (: iq) Agent.where (licence_to_kill: true) .minimum (: iq) 

SQL

SELECT MIN ("agents". "Iq") FROM "agents" SELECT MIN ("agents". "Iq") FROM "agents" WHERE "agents". "Licence_to_kill" =? [["licence_to_kill", "t"]]

Attention!

Toutes ces méthodes d'agrégation ne vous laissent pas enchaîner sur d'autres choses, elles sont terminales. L'ordre est important pour faire des calculs. Nous n'obtenons pas de ActiveRecord :: Relation objet de retour de ces opérations, ce qui fait que la musique s'arrête à ce moment-là, nous obtenons un hachage ou des chiffres. Les exemples ci-dessous ne fonctionneront pas:

Des rails

Agent.maximum (: number_of_gadgets) .where (licence_to_kill: true) Agent.sum (: number_of_gadgets) .where.not (licence_to_kill: true) Agent.joins (: mission) .average (: number_of_gadgets) .group ')

Groupé

Si vous souhaitez que les calculs soient décomposés et triés en groupes logiques, vous devez utiliser un GROUPE clause et ne pas le faire en Ruby. Ce que je veux dire par là, c'est que vous devriez éviter de parcourir un groupe qui produit potentiellement des tonnes de requêtes.

Des rails

Agent.joins (: mission) .group ('missions.name'). Average (: number_of_gadgets) # => "Moonraker" => 4.4, "Octopussy" => 4.9

SQL

SELECT AVG ("agents". "Number_of_gadgets") AS nombre_moyen_gadgets, missions.nom AS nom_missions FROM "agents" INNER JOIN "missions" ON "missions". "Id" = "agents". "Id_mission" GROUP BY missions.name

Cet exemple trouve tous les agents qui sont regroupés dans une mission particulière et renvoie un hachage avec le nombre moyen de gadgets calculé comme valeur - dans une seule requête! Ouaip! Il en va de même pour les autres calculs, bien sûr. Dans ce cas, il est plus logique de laisser SQL faire le travail. Le nombre de requêtes que nous envoyons pour ces agrégations est tout simplement trop important..

Chercheurs dynamiques

Pour chaque attribut de vos modèles, dites prénom, adresse électroniquefavori_gadget Et ainsi de suite, Active Record vous permet d'utiliser des méthodes de recherche très lisibles, créées dynamiquement pour vous. Ça a l'air cryptique, je sais, mais ça ne veut rien dire d'autre que find_by_id ou find_by_favorite_gadget. le trouver_par la partie est standard et Active Record se contente de vous attribuer le nom de l'attribut. Vous pouvez même arriver à ajouter un ! si vous voulez que ce chercheur génère une erreur si rien ne peut être trouvé. La partie malade est que vous pouvez même enchaîner ces méthodes de recherche dynamique. Juste comme ça:

Des rails

Agent.find_by_name ('James Bond') Agent.find_by_name_and_licence_to_kill ('James Bond', true)

SQL

SELECT "agents". * FROM "agents" O "agents". "Nom" =? LIMITE 1 [["" nom "," James Bond "]] SÉLECTIONNER" agents ". * FROM" agents "O" agents "." Nom "=? AND "agents". "Licence_to_kill" =? LIMITE 1 [["" nom "," James Bond "], [" licence_to_kill "," t "]]

Bien sûr, vous pouvez devenir fou avec cela, mais je pense qu'il perd son charme et son utilité si vous allez au-delà de deux attributs:

Des rails

Agent.find_by_name_and_licence_to_kill_and_womanizer_and_gambler_and_number_of_gadgets ('James Bond', true, true, true, 3) 

SQL

SELECT "agents". * FROM "agents" O "agents". "Nom" =? AND "agents". "Licence_to_kill" =? ET "agents". "Womanizer" =? ET "agents". "Joueur" =? AND "agents". "Number_of_gadgets" =? LIMITE 1 [["nom", "James Bond"], ["licence_to_kill", "t"], ["womanizer", "t"], ["joueur", "t"], ["nombre_de_gadgets", 3 ]]

Dans cet exemple, il est néanmoins agréable de voir comment cela fonctionne sous le capot. Chaque nouvelle _et_ ajoute un SQL ET opérateur pour relier logiquement les attributs. Globalement, le principal avantage des outils de recherche dynamiques est la lisibilité. Le fait de masquer un trop grand nombre d'attributs dynamiques perd rapidement cet avantage. Je l'utilise rarement, peut-être surtout lorsque je joue dans la console, mais il est certainement bon de savoir que Rails propose cette jolie petite supercherie..

Champs spécifiques

Active Record vous permet de renvoyer des objets un peu plus ciblés sur les attributs qu’ils véhiculent. Habituellement, sauf indication contraire, la requête demandera tous les champs dans une ligne via * (SELECT "agents". *), puis Active Record construit les objets Ruby avec l’ensemble complet d’attributs. Cependant, vous pouvez sélectionner seuls les champs spécifiques qui doivent être renvoyés par la requête et limiter le nombre d'attributs que vos objets Ruby doivent «transporter».

Des rails

Agent.select ("name") => #, #,…]>

SQL

SELECT "agents". "Nom" FROM "agents"

Des rails

Agent.select ("number, favorite_gadget") => #, #,…]>

SQL

SELECT "agents". "Numéro", "agents". "Favori_gadget" FROM "agents"

Comme vous pouvez le constater, les objets renvoyés n’auront que les attributs sélectionnés, ainsi que leurs identifiants de cours, ce qui est une donnée pour tout objet. Cela ne fait aucune différence si vous utilisez des chaînes, comme ci-dessus, ou des symboles - la requête sera la même.

Des rails

Agent.select (: number_of_kills) Agent.select (: name,: licence_to_kill)

Attention: si vous essayez d’accéder à des attributs de l’objet que vous n’avez pas sélectionnés dans vos requêtes, vous recevrez un message. MissingAttributeError. Depuis le identifiant sera automatiquement fourni pour vous de toute façon, vous pouvez demander l’identifiant sans le sélectionner, bien que.

SQL personnalisé

Enfin, vous pouvez écrire votre propre code SQL personnalisé via find_by_sql. Si vous êtes suffisamment confiant dans votre propre SQL-Fu et avez besoin d'appels personnalisés à la base de données, cette méthode peut s'avérer très pratique parfois. Mais c'est une autre histoire. N'oubliez pas de vérifier d'abord les méthodes d'emballage Active Record et évitez de réinventer la roue dans laquelle Rails tente de vous rencontrer à plus de la moitié..

Des rails

Agent.find_by_sql ("SELECT * FROM agents") Agent.find_by_sql ("Nom SELECT, licence_to_kill FROM agents") 

Sans surprise, cela se traduit par:

SQL

SELECT * FROM agents SELECT nom, licence_to_kill FROM agents

Étant donné que les portées et vos propres méthodes de classe peuvent être utilisées de manière interchangeable pour vos besoins de recherche personnalisés, nous pouvons aller encore plus loin pour les requêtes SQL plus complexes.. 

Des rails

agent de classe < ActiveRecord::Base… def self.find_agent_names query = <<-SQL SELECT name FROM agents SQL self.find_by_sql(query) end end

Nous pouvons écrire des méthodes de classe qui encapsulent le code SQL dans un document Here. Cela nous permet d’écrire des chaînes multilignes de façon très lisible, puis de stocker cette chaîne SQL dans une variable que nous pouvons réutiliser et passer dans. find_by_sql. De cette façon, nous ne mettons pas beaucoup de code de requête dans l'appel de la méthode. Si vous avez plus d'un endroit pour utiliser cette requête, il est également sec.

Etant donné que ceci est supposé être convivial pour les débutants et non pas un tutoriel SQL, j'ai gardé l'exemple très minimaliste pour une raison. La technique utilisée pour les requêtes beaucoup plus complexes est toutefois pratiquement la même. Il est facile d’imaginer avoir une requête SQL personnalisée qui dépasse les dix lignes de code.. 

Allez aussi fou que vous avez besoin de raisonnablement! Cela peut sauver la vie. Un mot sur la syntaxe ici. le SQL la partie est juste un identifiant ici pour marquer le début et la fin de la chaîne. Je parie que vous n'aurez pas besoin de cette méthode, espérons-le! Il a définitivement sa place, et Rails Land ne serait pas pareil sans elle. Dans les rares cas où vous voudriez absolument peaufiner votre propre code SQL avec celui-ci..

Dernières pensées

J'espère que vous serez un peu plus à l'aise pour rédiger des requêtes et lire le redoutable code SQL brut. La plupart des sujets abordés dans cet article sont essentiels à la rédaction de requêtes portant sur une logique métier plus complexe. Prenez votre temps pour les comprendre et jouer un peu avec les requêtes dans la console. 

Je suis presque certain que lorsque vous quitterez le didacticiel, votre crédit Rails augmentera considérablement si vous travaillez sur vos premiers projets réels et que vous devez élaborer vos propres requêtes personnalisées. Si vous êtes encore un peu timide sur le sujet, je dirais simplement de vous amuser avec ça - ce n'est vraiment pas sorcier!