Go to blue arrow
back to Tech Blog
Développement

Written by:

Pedro Rolo
Pedro Rolo

,

Développeur Rails

Tiago Madeira
Tiago Madeira

,

Développeur

Last Published:

28 avril 2021

•

Min Read

Requêtes sur Rails - Active Record and Arel

Pièces de puzzle colorées éparpillées en tas désordonné, illustrant des requêtes complexes sur Rails.

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 :

blue arrow to the left
Imaginary Cloud logo

Notre modèle de domaine

Nous utilisons le modèle de domaine suivant dans nos exemples :

Venn diagram showing the shaded intersection of Records and Associated Records for Rails database queries.
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 end

En 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.

blue arrow to the left
Imaginary Cloud logo

Qu'est-ce qu'ActiveRecord

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.

Requêtes simples ActiveRecord

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'

Requêtes composables ActiveRecord

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 1

Ou 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 10

Négation de prédicats

Il 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'

Tri

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" ASC

Regroupement

Il 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é ...
blue arrow to the left
Imaginary Cloud logo

Jointures ActiveRecord

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.

Les jointures externes gauches avec #includes

Venn diagram showing the shaded intersection of Records and Associated Records for Rails database queries.

#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"

Jointures internes avec #joins

Venn diagram showing the shaded intersection of Records and Associated Records for Rails database queries.

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"

Interroger les associations ActiveRecord avec des valeurs spécifiques

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"

blue arrow to the left
Imaginary Cloud logo

Abstractions de requêtes réutilisables avec des méthodes de classe

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 }) end

Aprè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'
blue arrow to the left
Imaginary Cloud logo

Qu'est-ce qu'Arel ?

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.

Table Arel

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 []:

Prédicats de requête Arel simples

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'

Correspondances Arel (SQL LIKE)

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 ».

blue arrow to the left
Imaginary Cloud logo

Conclusion

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.

Publicité pour un audit UX listant les bénéfices pour l'expérience utilisateur, avec des interfaces 3D.

Vous avez trouvé cet article utile ? Ceux-là vous plairont peut-être aussi !

blue arrow to the left
Imaginary Cloud logo
blue arrow to the left
Imaginary Cloud logo
blue arrow to the left
Imaginary Cloud logo
Pedro Rolo
Pedro Rolo

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

‍

LinkedIn

Read more posts by this author
Tiago Madeira
Tiago Madeira

Étudiant en informatique et à temps partiel d'ImaginaryCloud. Désireux d'apprendre de nouvelles technologies et techniques. Joueur de tennis et pianiste.

Read more posts by this author

People who read this post, also found these interesting:

Dropdown caret icon