Skip to content

Les associations en Rails: Partie 04

Bouzid Badreddine edited this page Oct 15, 2020 · 3 revisions

has_many :through

La combinaison de has_many avec :through est utilisé à deux fins:

  • Pour construire des méthodes d'associations raccourcies.
  • Pour construire des relations N:N

Méthodes d'associations raccourcies

Supposons par exemple qu'un auteur has_many livres, et que chaque livre has_many chapitres, pour accéder à tout les chapitres de l'auteur ahmed par exemple, il faudra retrouver tout les chapitres des livres un à un, par exemple ahmed.book1.chapters + ahmed.book2.chapters + ...., cela est peu pratique si l'auteur à un grand nombre de livres, ou si ce nombre est non connu.

On aimerait bien retrouver ces chapitres simplement en appelant ahmed.chapters, cela est possible grâce à :through.

D'abord générons les modèles:

#!ruby

rails generate model author name:string
rails generate model book title:string author:belongs_to
rails generate model chapter title:string book:belongs_to

Maintenant, seul les méthodes belongs_to sont ajouté automatiquement par Rails aux classes Book et Chapter, on doit nous-même ajouter les méthodes has_many:

  • Un auteur a plusieurs livres
  • Un livre a plusieurs chapitres
  • Donc, un auteur a plusieurs chapitres, mais ces chapitres sont déduits via ses livres

Les trois modèles seront donc:

#!ruby

class Chapter < ApplicationRecord
  belongs_to :book
end
#!ruby

class Book < ApplicationRecord
  has_many :chapters
  belongs_to :author
end
#!ruby

class Author < ApplicationRecord
  has_many :books
  has_many :chapters, through: :books
end

Voyons ce que donne cette structure:

#!ruby

2.4.0 :001 > ahmed = Author.new name: "Ahmed"
 => #<Author id: nil, name: "Ahmed", created_at: nil, updated_at: nil> 
2.4.0 :002 > ahmed.books.any?
 => false 
2.4.0 :003 > ahmed.books.build title: "Quantum Mechanics"
 => #<Book id: nil, title: "Quantum Mechanics", author_id: nil, created_at: nil, updated_at: nil> 
2.4.0 :004 > ahmed.books.first.title                     
 => "Quantum Mechanics" 
2.4.0 :005 > ahmed.books.first.chapters.any?
 => false 
2.4.0 :006 > ahmed.books.first.chapters.build title: "QM 1"
 => #<Chapter id: nil, title: "QM 1", book_id: nil, created_at: nil, updated_at: nil> 
2.4.0 :007 > ahmed.books.first.chapters.build title: "QM 2"
 => #<Chapter id: nil, title: "QM 2", book_id: nil, created_at: nil, updated_at: nil> 
2.4.0 :008 > ahmed.books.first.chapters.build title: "QM 3"
 => #<Chapter id: nil, title: "QM 3", book_id: nil, created_at: nil, updated_at: nil> 
2.4.0 :009 > ahmed.books.first.chapters.size               
 => 3 
2.4.0 :010 > ahmed.books.build title: "Relativity"         
 => #<Book id: nil, title: "Relativity", author_id: nil, created_at: nil, updated_at: nil> 
2.4.0 :011 > ahmed.books.second.title             
 => "Relativity" 
2.4.0 :012 > ahmed.books.second.chapters.build [ { title: "REL 1" }, { title: "REL 2" } ]
 => [#<Chapter id: nil, title: "REL 1", book_id: nil, created_at: nil, updated_at: nil>, #<Chapter id: nil, title: "REL 2", book_id: nil, created_at: nil, updated_at: nil>] 
2.4.0 :013 > ahmed.books.second.chapters.size                                            
 => 2 
2.4.0 :014 > ahmed.books.first.chapters.size 
 => 3 
2.4.0 :015 > ahmed.chapters.size            
 => 0 
2.4.0 :016 > ahmed.save         
   (0.2ms)  begin transaction
  SQL (0.5ms)  INSERT INTO "authors" ("name", "created_at", "updated_at") VALUES (?, ?, ?)  [["name", "Ahmed"], ["created_at", "2017-07-13 18:42:40.132135"], ["updated_at", "2017-07-13 18:42:40.132135"]]
  SQL (0.3ms)  INSERT INTO "books" ("title", "author_id", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["title", "Quantum Mechanics"], ["author_id", 2], ["created_at", "2017-07-13 18:42:40.134597"], ["updated_at", "2017-07-13 18:42:40.134597"]]
  SQL (0.2ms)  INSERT INTO "chapters" ("title", "book_id", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["title", "QM 1"], ["book_id", 3], ["created_at", "2017-07-13 18:42:40.136021"], ["updated_at", "2017-07-13 18:42:40.136021"]]
  SQL (0.1ms)  INSERT INTO "chapters" ("title", "book_id", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["title", "QM 2"], ["book_id", 3], ["created_at", "2017-07-13 18:42:40.137154"], ["updated_at", "2017-07-13 18:42:40.137154"]]
  SQL (0.1ms)  INSERT INTO "chapters" ("title", "book_id", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["title", "QM 3"], ["book_id", 3], ["created_at", "2017-07-13 18:42:40.137867"], ["updated_at", "2017-07-13 18:42:40.137867"]]
  SQL (0.1ms)  INSERT INTO "books" ("title", "author_id", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["title", "Relativity"], ["author_id", 2], ["created_at", "2017-07-13 18:42:40.138764"], ["updated_at", "2017-07-13 18:42:40.138764"]]
  SQL (0.1ms)  INSERT INTO "chapters" ("title", "book_id", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["title", "REL 1"], ["book_id", 4], ["created_at", "2017-07-13 18:42:40.139562"], ["updated_at", "2017-07-13 18:42:40.139562"]]
  SQL (0.1ms)  INSERT INTO "chapters" ("title", "book_id", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["title", "REL 2"], ["book_id", 4], ["created_at", "2017-07-13 18:42:40.140328"], ["updated_at", "2017-07-13 18:42:40.140328"]]
   (131.2ms)  commit transaction
 => true 
2.4.0 :017 > ahmed.reload.chapters.size
  Author Load (0.7ms)  SELECT  "authors".* FROM "authors" WHERE "authors"."id" = ? LIMIT ?  [["id", 2], ["LIMIT", 1]]
   (0.2ms)  SELECT COUNT(*) FROM "chapters" INNER JOIN "books" ON "chapters"."book_id" = "books"."id" WHERE "books"."author_id" = ?  [["author_id", 2]]
 => 5 
2.4.0 :019 > ahmed.reload.chapters.each { |chap| puts " - " + chap.title }
  Author Load (0.4ms)  SELECT  "authors".* FROM "authors" WHERE "authors"."id" = ? LIMIT ?  [["id", 2], ["LIMIT", 1]]
  Chapter Load (0.2ms)  SELECT "chapters".* FROM "chapters" INNER JOIN "books" ON "chapters"."book_id" = "books"."id" WHERE "books"."author_id" = ?  [["author_id", 2]]
 - QM 1
 - QM 2
 - QM 3
 - REL 1
 - REL 2

Nous voyons donc que grâce à has_many :chapters, through: :books les instances de Author se sont vu dotées des méthodes .chapters et .chapters= qui permettent d'accéder directement à tout les chapitres des livres associés.

ERD

Associations N:N

Imaginons la situation suivante: On a plusieurs médecins qui ont plusieurs RDVs avec plusieurs patients, donc chaque médecin est associé à plusieurs patients. Mais rien n'empêche qu'un patient ait des RDVs avec plusieurs médecins traitants. C'est un cas typique de relation N:N.

Il y aura trois tables: Praticians, Appointments et Patients. Et c'est la deuxième qui servira de 'table de jonction' entre les deux tables, vu qu'elle comportera deux clés étrangères, une référant au médecin, et l'autre au patient.

Voyons comment ça marche:

D'abord on génère les modèles:

#!ruby

rails generate model physician name:string
rails generate model patient name:string
rails genetate model appointment date:date physician:belongs_to patient_belongs_to

Ensuite, on ajoute les has_many et has_many :through:

#!ruby

class Physician
  has_many :appointments
  has_many :patients, through: :appointments
end
#!ruby

class Chapter
  has_many :appointments
  has_many :physicians, through: :appointments
end

La classe Chapter comprend déjà les belongs_to (généré automatiquement), et n'a pas a être éditée:

#!ruby

class Appointment
  belongs_to :physician
  belongs_to :patient
end

Maintenant prenons un exemple concret:

#!ruby

2.4.0 :001 > dr_ahmed = Physician.new name: "Dr. Ahmed"
2.4.0 :002 > dr_hichem = Physician.new name: "Dr. Hichem"
2.4.0 :003 > dr_farid = Physician.new name: "Dr. Hichem"
2.4.0 :004 > khadidja = Patient.new name: "Khadidja"
2.4.0 :005 > keltoum = Patient.new name: "Keltoum"
2.4.0 :006 > zahra = Patient.new name: "Zahra"
2.4.0 :007 > dr_ahmed.appointments.any?
 => false 
2.4.0 :008 > dr_ahmed.appointments.build [ \
2.4.0 :009 >     { date: 2.days.from_now, patient: khadidja }, \
2.4.0 :010 >     { date: 1.week.from_now, patient: keltoum } ]
 => [...] 
2.4.0 :011 > dr_hichem.appointments.build [ \
2.4.0 :012 >     { date: 2.weeks.from_now, patient: keltoum }, \
2.4.0 :013 >     { date: 1.day.from_now, patient: zahra }, \
2.4.0 :014 >     { date: 3.days.from_now, patient: khadidja } ]
 => [...] 
2.4.0 :015 > dr_farid.appointments.build date: 5.hours.from_now, patient: keltoum
 => ...
2.4.0 :016 > dr_hichem.save
   (0.2ms)  begin transaction
   ...
2.4.0 :019 > dr_ahmed.reload.patients.each { |p| puts p.name }
  Physician Load (0.3ms)  ...
Khadidja
Keltoum
 => [...] 
2.4.0 :020 > dr_hichem.reload.patients.each { |p| puts p.name }
  Physician Load (0.3ms)  ...
Keltoum
Zahra
Khadidja
 => [...] 
2.4.0 :021 > dr_farid.reload.patients.each { |p| puts p.name } 
  Physician Load (0.3ms)  ...
Keltoum
 => [...] 
2.4.0 :023 > khadidja.reload.physicians.each { |p| puts " >> " + p.name }
  Patient Load (0.4ms)  ...
 >> Dr. Hichem
 >> Dr. Ahmed
 => [...] 
2.4.0 :024 > keltoum.reload.physicians.each { |p| puts " >> " + p.name } 
  Patient Load (0.3ms)  ...
 >> Dr. Hichem
 >> Dr. Ahmed
 >> Dr. Hichem
 => [...] 
2.4.0 :025 > zahra.reload.physicians.each { |p| puts " >> " + p.name }  
  Patient Load (0.3ms)  ...
 >> Dr. Hichem
 => [...] 

ERD