contactez nous


Cet article a été initialement publié en juillet 2018 par Pedro Rolo et mis à jour en juin 2020 par Tiago Madere .
Il existe plusieurs fonctionnalités qui font Ruby on Rails attrayant, mais là où il brille vraiment, c'est lorsqu'il s'agit d'obtenir des informations à partir de bases de données relationnelles. C'est ORM -Actif recording - propose désormais une interface très mature, trouvant le juste équilibre entre concision et puissance.
Dans cet article, je vais fournir quelques exemples simples de la façon d'extraire des informations d'une base de données. Il ne s'agit pas d'un guide complet, mais plutôt d'un ensemble d'outils que je trouve utiles dans mon travail quotidien. De plus, ce que je présente ci-dessous est basé sur la dernière mise à jour de Rails (Rails 6).
Je vais commencer par définir un modèle de domaine par rapport auquel je vais exécuter mes requêtes :
Nous utilisons le modèle de domaine suivant dans nos exemples :

class Country < ApplicationRecord
has_many :people
has_many :companies
end
class Person < ApplicationRecord
belongs_to :country
has_many :company_people
has_many :companies, through: :company_people
end
class Company < ApplicationRecord
belongs_to :country
has_many :company_people
has_many :people, through: :company_people
end
class CompanyPerson < ApplicationRecord
belongs_to :company
belongs_to :person
endEn résumé, Person et Company appartiennent à un Country, et Person et Company entretiennent une relation plusieurs-à-plusieurs, stockée dans la table de relation CompanyPerson.
Le #activerecord est ce qui lie le modèle de notre application Ruby à sa table de base de données correspondante. L'objet Active Record est fourni avec un ensemble de méthodes pour nous aider à interroger les enregistrements de la base de données, il n'est donc pas nécessaire d'utiliser du SQL brut. À des fins de comparaison, nous présenterons la traduction de nos requêtes ActiveRecord en requêtes SQL. Vous pouvez facilement effectuer cette traduction en utilisant #to_sql.
Les requêtes les plus simples ne concernent qu'une seule table et visent à obtenir les enregistrements qui possèdent certaines valeurs spécifiques dans leurs colonnes.
Par exemple, disons que nous voulons trouver qui a John comme prénom :
Person.where(first_name: "John")
#=> #<ActiveRecord::Relation [#<Person id: 6941, first_name: "John", last_name: "Swift", birth_date: "1986-04-30", country_id: 68, created_at: "2018-05-28 16:32:50", updated_at: "2018-05-28 16:32:50">]>
# Nous pouvons afficher nos requêtes en utilisant #to_sql
Person.where(first_name: "John").to_sql
# SELECT "people".* FROM "people"
# WHERE "people"."first_name" = 'John'Nous pouvons également utiliser des tableaux de valeurs souhaitées au lieu de valeurs uniques. Cela générera des requêtes utilisant le IN mot-clé SQL :
Person.where(first_name: ["Peter", "John"]).to_sql
# SELECT "people".* FROM "people"
# WHERE "people"."first_name" IN ('Peter', 'John')
Person.where(first_name: ["Peter", "John"])
#=> #<ActiveRecord::Relation [#<Person id: 5039, first_name: "Peter", last_name: "Corkery", birth_date: "1910-10-30", country_id: 63, created_at: "2018-05-28 16:32:19", updated_at: "2018-05-28 16:32:19">, #<Person id: 6941, first_name: "John", last_name: "Swift", birth_date: "1986-04-30", country_id: 68, created_at: "2018-05-28 16:32:50", updated_at: "2018-05-28 16:32:50">]>En plus des tableaux, les plages (ranges) sont également acceptées comme prédicats de valeur :
Person.where(birth_date: (10.years.ago..5.days.ago))
# SELECT "people".* FROM "people" WHERE "people"."birth_date"
# BETWEEN '2008-05-30' AND '2018-05-25'Vous pouvez également utiliser des plages infinies :
# et pour les objets, nous pouvons utiliser l'infini dans les plages
Person.where(birth_date: (10.years.ago..Date::Infinity.new))
# SELECT "people".* FROM "people" WHERE "people"."birth_date" >= '2008-05-28'qui, désormais avec Rails 6, peut simplement s'écrire comme suit :
# et pour les objets, nous pouvons utiliser l'infini dans les plages
Person.where(birth_date: (10.years.ago..))
# SELECT "people".* FROM "people" WHERE "people"."birth_date" >= '2008-05-28'Il est possible de restreindre nos requêtes à plusieurs prédicats simultanés. Nous pouvons soit fournir #where un hash avec plusieurs mots-clés, soit enchaîner plusieurs appels à #where:
Person.where(first_name: ["Peter","John"],
last_name: "Swift")
.to_sql
# SELECT "people".* FROM "people"
# WHERE "people"."first_name" IN ('Peter', 'John')
# AND "people"."last_name" = 'Swift'
Person.where(first_name: ["Peter","John"])
.where(last_name: "Swift")
.to_sql
# SELECT "people".* FROM "people"
# WHERE "people"."first_name" IN ('Peter', 'John')
# AND "people"."last_name" = 'Swift'Si, au contraire, nous souhaitons composer nos prédicats avec un OR , nous pouvons utiliser #or.
are_swifts = Person.where(last_name: "Swift")
Person.where(first_name: ["Peter","John"])
.or(are_swifts)
.to_sql
# SELECT "people".* FROM "people"
# WHERE ("people"."first_name" IN ('Peter', 'John')
# OR "people"."last_name" = 'Swift')Lorsque nous recherchons une ligne spécifique, nous pouvons utiliser #find_by à la place, ce qui arrête la recherche dès la première correspondance trouvée en utilisant LIMIT 1
Person.find_by(first_name: "Peter").to_sql
# SELECT "people".* FROM "people"
# WHERE "people"."first_name" = 'Peter'
# LIMIT 1Ou nous pouvons définir notre propre limite personnalisée en enchaînant #limit vers notre #where requêtes :
Person.where(first_name: "Peter").limit(10).to_sql
# SELECT "people".* FROM "people"
# WHERE "people"."first_name" = 'Peter'
# LIMIT 10Il est possible d'obtenir les enregistrements ne correspondant pas à une condition donnée en utilisant #not avec #where:
Person.where.not(first_name: "Peter").to_sql
# SELECT "people".* FROM "people"
# WHERE "people"."first_name" != 'Peter'
Person.where(first_name: "Peter")
.where.not(last_name: "Swift")
.where(birth_date: 30.years.ago)
.to_sql
# SELECT "people".* FROM "people"
# WHERE "people"."first_name" = 'Peter'
# AND "people"."last_name" != 'Swift'
# AND "people"."birth_date" = '1988-05-28'Il est possible d'utiliser #order pour trier les résultats par ordre croissant ou décroissant :
Person.where.not(last_name: "Swift")
.order(birth_date: :desc,
created_at: :asc)
.to_sql
# SELECT "people".* FROM "people"
# WHERE "people"."last_name" != 'Swift'
# ORDER BY "people"."birth_date" DESC,
# "people"."created_at" ASCIl existe plusieurs fonctions permettant de condenser le résultat d'une requête en certaines valeurs, telles que #count, #maximum, #minimum et #average:
Person.count # => 500
# SELECT COUNT(*) FROM "people"
Person.average(:birth_date) # => Mon, 01 Jan 1962
# SELECT AVG("people"."birth_date") FROM "people"
Person.maximum(:birth_date) # => Sat, 26 May 2018
# SELECT MAX("people"."birth_date") FROM "people"
Person.minimum(:birth_date) # => Mon, 29 Jun 1908
# SELECT MIN("people"."birth_date") FROM "people"Il est également possible de regrouper des éléments par une colonne donnée et d'agréger ces éléments avec ces fonctions de calcul, permettant ainsi d'effectuer des GROUP_BY :
Person.group(:last_name).count
# SELECT COUNT(*) AS count_all,
# "people"."last_name" AS people_last_name
# FROM "people"
# GROUP BY "people"."last_name"
# => {"Abbott"=>13, "Abernathy"=>12, "Abshire"=>16, "Adams"=>18,
# ... résultat tronqué ...
Person.group(:last_name)
.average(:birth_date)
# SELECT AVG("people"."birth_date") AS average_birth_date,
# "people"."last_name" AS people_last_name
# FROM "people" GROUP BY "people"."last_name"
# => {"Abbott"=>#<BigDecimal:7fd61693d160,'0.1959307692 30769E4',27(36)>,
# "Abernathy"=>#<BigDecimal:7fd61693cda0,'0.19655E4',18(36)>,
# ... résultat tronqué ...Le cas d'utilisation le plus courant impliquant plusieurs tables concerne le chargement anticipé (eager loading) d'entités liées à notre modèle sélectionné, qui pourraient, par exemple, être accédées de manière itérative. Ce problème est communément appelé le problème des requêtes 1 + N et l'interaction suivante le met en évidence :
lazy_people = Person.where(last_name: "Smith")
lazy_people.map(&:country).map(&:name)
# SELECT "people".* FROM "people"
# WHERE "people"."last_name" = 'Smith'
# SELECT "countries".* FROM "countries"
# WHERE "countries"."id" = 42 LIMIT 1
# SELECT "countries".* FROM "countries"
# WHERE "countries"."id" = 60 LIMIT 1
# SELECT "countries".* FROM "countries"
# WHERE "countries"."id" = 70 LIMIT 1
# SELECT "countries".* FROM "countries"
# WHERE "countries"."id" = 68 LIMIT 1
# SELECT "countries".* FROM "countries"
# WHERE "countries"."id" = 67 LIMIT 1
# SELECT "countries".* FROM "countries"
# WHERE "countries"."id" = 64 LIMIT 1
# ...
# => ["République tchèque", "Wallis-et-Futuna", "Belize", "Cuba", "Koweït",
# "Libéria", ...]
Comme nous pouvons le constater, une nouvelle requête est exécutée à chaque itération sur une personne pour accéder à son pays.
#includes
#includes nous permettent de charger toutes les données en une seule fois, optimisant ainsi l'opération précédente. Cela se comporte comme une LEFT OUTER JOIN, chargeant ainsi toutes les entrées du modèle associé qui concernent nos entrées du modèle principal.
eager_people = Person.where(last_name: "Smith")
.includes(:country)
# SELECT "people".* FROM "people"
# WHERE "people"."last_name" = 'Smith'
# SELECT "countries".* FROM "countries"
# WHERE "countries"."id" IN (42, 60, 70, 80, 95, 102, 105, 200)
# et nous pouvons désormais itérer sur les modèles et les relations
# sans avoir à effectuer de requêtes supplémentaires afin
# d'extraire, par exemple, le nom de chaque pays
eager_people.map(&:country).map(&:name)
# => ["République tchèque", "Wallis-et-Futuna", "Belize", "Cuba", ...]
Mais attendez, ce n'est pas une LEFT OUTER JOIN , me direz-vous ! Eh bien, comme il n'y a aucune contrainte supplémentaire sur les pays que nous sélectionnons, Rails préfère recourir à une WHERE id IN requête, en tirant parti de l'index de la clé primaire. Cependant, si nous ajoutons des contraintes à notre requête, Active Record effectue une LEFT OUTER JOIN à la place :
eager_people = Person.includes(:country)
.where(countries: {
gdp: -Float::INFINITY..1000000})
# SELECT "people"."id" AS t0_r0,
# "people"."first_name" AS t0_r1,
# "people"."last_name" AS t0_r2,
# "people"."birth_date" AS t0_r3,
# "people"."country_id" AS t0_r4,
# "people"."created_at" AS t0_r5,
# "people"."updated_at" AS t0_r6,
# "countries"."id" AS t1_r0,
# "countries"."name" AS t1_r1,
# "countries"."gdp" AS t1_r2,
# "countries"."created_at" AS t1_r3,
# "countries"."updated_at" AS t1_r4 FROM "people"
# LEFT OUTER JOIN "countries" ON "countries"."id" = "people"."country_id"
# WHERE "countries"."gdp" <= 1000000
# => ["République tchèque", "Wallis-et-Futuna", "Belize", "Cuba",...]
Ce qu'il faut remarquer dans l'exemple ci-dessus, c'est l'utilisation d'un hash dans la condition where afin de spécifier des contraintes sur nos modèles référencés.
Nous pouvons également charger plusieurs relations en même temps :
Person.includes(:country, :companies)
# SELECT "people".* FROM "people"
# SELECT "countries".* FROM "countries"
# WHERE "countries"."id" IN (5, 34, 50, 60)
# SELECT "company_people".* FROM "company_people"
# WHERE "company_people"."person_id" IN (24, 10, 41, 35)
# SELECT "companies".* FROM "companies"
# WHERE "companies"."id" IN (3, 32, 23, 65, 23)
Il est également possible de le faire de manière imbriquée, en passant plutôt un hash de relations :
Person.includes(country: :companies)
# SELECT "people".* FROM "people" LIMIT ? [["LIMIT", 11]]
# SELECT "countries".* FROM "countries"
# WHERE "countries"."id" IN (23,34,6,34,3,2,6,7)
# SELECT "companies".* FROM "companies"
# WHERE "companies"."country_id" IN (5,3,7,3,4,2,1)
# SELECT "people".* FROM "people"
#joins
Parfois, nous ne nous intéressons qu'aux enregistrements associés au module correspondant. Dans ce cas, nous utilisons #joins, qui possède la sémantique d'une INNER JOIN.
Nous illustrons le besoin de cette fonctionnalité dans l'exemple suivant, où nous souhaitons uniquement parcourir les personnes réellement associées à un pays :
# étant donné un certain ferdinand sans pays
ferdinand_nobody = Person.find_by(country_id: nil)
# => #<Person id: 7042, first_name: "Fernando", last_name: "Pessoa",
# birth_date: "1888-06-13", country_id: nil,
# created_at: "2018-05-30 13:37:03", updated_at: "2018-05-30 13:37:03">
# lors de l'interrogation des noms des pays des personnes
Person.includes(:country).map(&:country).map(&:name)
# NoMethodError: undefined method `name' for nil:NilClass
# depuis (irb):27:in `map'
# depuis (irb):27
# car notre ferdinand est dans cette liste
Person.includes(:country).include?(ferdinand_nobody) #=> true
# et son pays est nul
ferdinand_nobody.country #=> nil
# #joins à la rescousse
Person.joins(:country).include?(ferdinand_nobody) #=> false
# SELECT "people".* FROM "people"
# INNER JOIN "countries"
# ON "countries"."id" = "people"."country_id"
#=> tous les noms de pays des personnes
Person.joins(:country).map(&:country).map(&:name)
# SELECT "people".* FROM "people"
# INNER JOIN "countries"
# ON "countries"."id" = "people"."country_id"
Si nous ne nous intéressons qu'aux enregistrements associés à un modèle lié possédant des valeurs d'attributs spécifiques, nous pouvons utiliser #joins puis #où pour spécifier des contraintes sur nos modèles.
Person.joins(:country).where(countries: { gdp: 110 })
#"SELECT "people".* FROM "people"
#INNER JOIN "countries"
#ON "countries"."id" = "people"."country_id"
#WHERE "countries"."gdp" = 110"
Toutes les requêtes écrites jusqu'ici sont assez longues. Lorsqu'elles deviennent complexes ou qu'elles sont destinées à être réutilisées dans notre code, il est pratique de leur donner un nom.
Dans les anciennes versions de Rails, vous devriez utiliser la ActiveRecord::Base#scope méthode de classe pour créer de telles requêtes réutilisables. Vous pouvez obtenir la même fonctionnalité en définissant simplement des méthodes de classe :
def Person.adults
_18_years_ago = (Time.now - 18.years)
Person.where(
birth_date: _18_years_ago..Date::Infinity.new)
end
def Person.from_big_economies
Person
.joins(:country)
.where(countries: {
gdp: 100_000.0..Float::INFINITY
})
endAprès avoir défini les requêtes de cette manière, vous pouvez enchaîner ces abstractions de haut niveau.
Person.from_big_economies.adults
# SELECT "people".* FROM "people"
# INNER JOIN "countries"
# ON "countries"."id" = "people"."country_id"
# WHERE "countries"."gdp" >= 100000
# AND "people"."birth_date" >= '2000-07-04'Arel est un langage dédié (DSL) permettant d'exprimer vos requêtes en algèbre relationnelle. Dans les anciennes versions de Rails, il était assez courant de devoir recourir à Arel pour accomplir certaines fonctionnalités fréquemment demandées, bien qu'aujourd'hui, l'Active Record de Rails 6 couvre déjà la plupart de ces cas d'utilisation.
Je commencerai par présenter certaines des requêtes Rails 6 déjà abordées en utilisant Arel au lieu d'Active Record classique. Je me concentrerai ensuite sur la manière d'écrire certains cas où Arel reste nécessaire.
Lors de la création de requêtes Arel, on commence généralement par récupérer les Arel::Table objets des tables SQL avec lesquelles nous souhaitons interagir :
people = People.arel_table
# => #<Arel::Table:0x007ffc5def7c50 @name="people" ...>
countries = Country.arel_table
# => #<Arel::Table:0x007ffc5dcb2328 @name="countries" ...>Nous pouvons accéder aux colonnes de nos tables comme à un hash. À partir de ces tables, il est possible de définir des prédicats (Arel::Node) en invoquant des méthodes sur leurs attributs, accessibles via la méthode []:
Les prédicats #eq, #not_eq, #lt, #gt, #lteq, #gteq sont équivalents aux opérateurs =, !=, <, >, <=, >= et fonctionnent comme suit :
countries_predicate = countries[:gdp].eq(10000)
# => #<Arel::Nodes::Equality:0x00007fd0141a29d0 ...>
people_lt_predicate = people[:birth_date].lt(Time.now - 10.years)
# => #<Arel::Nodes::LessThan:0x00007fd00baa0798 ...>
people_gt_predicate = people[:birth_date].gt(Time.now - 20.years)
# => #<Arel::Nodes::GreaterThan:0x007ffc5e1c47d0 ...>Les #and & #or peuvent être utilisés pour créer des prédicats multi-tables et fonctionnent de manière similaire, comme ceci :
or_predicate = countries[:gdp].gt(10000).or(people[:birth_date].gt(Time.now - 10.years))
# => #<Arel::Nodes::Grouping:0x00007fd0141d1ca8 ...>
and_predicate = countries[:gdp].gt(1000).and(countries[:gdp].lt(2000))
# => #<Arel::Nodes::And:0x00007fd0138b4648 ...>Nous pouvons ensuite transmettre ces prédicats à #where, et tant que les tables requises sont jointes, le prédicat sera converti avec succès en SQL :
Person.joins(:country)
.where(or_predicate)
# => SELECT "people".* FROM "people"
# INNER JOIN "countries"
# ON "countries"."id" = "people"."country_id"
# WHERE "countries"."gdp" > 10000
# OR "people"."birth_date" > '2008-07-04'Il s'agit d'un cas d'utilisation où il est encore nécessaire de recourir à Arel pour obtenir une requête indépendante de la base de données. Pour ce faire, il convient de combiner les #matches Prédicat Arel avec le % caractère joker SQL :
people_last_name = Person.arel_table[:last_name]
Person.where(people_last_name.matches("A%"))
# SELECT "people".* FROM "people"
# WHERE "people"."last_name" LIKE 'A%'
# => #<ActiveRecord::Relation [#<Person last_name: "Aufderhar" ... >, #<Person last_name: "Armstrong"...> ...]>Ceci renvoie les personnes dont le nom de famille commence par la lettre « A ».
Arel et Active Record proposent bien plus de fonctionnalités que celles que j'ai présentées ici. Cependant, ils sont largement suffisants pour démarrer avec des applications aux exigences simples.
En fin de compte, il s'agit de choisir de préférence entre les méthodes de requête ActiveRecord ou Arel, lorsque nous effectuons nos requêtes quotidiennes. Je trouve la syntaxe ActiveRecord plus facile à lire et à utiliser, mais Arel peut toujours être utile pour traiter des requêtes complexes.


Développeur de rails avec plus de 10 ans d'expérience dans diverses technologies. Je m'intéresse à la programmation fonctionnelle.

Étudiant en informatique et à temps partiel d'ImaginaryCloud. Désireux d'apprendre de nouvelles technologies et techniques. Joueur de tennis et pianiste.
People who read this post, also found these interesting: