-
Notifications
You must be signed in to change notification settings - Fork 0
Les associations en Rails: Partie 04
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
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.
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
=> [...]