Examining Many-To-Many Relationships In Rails: A Quest
In the 1975 cult classic Monty Python and the Holy Grail, King Arthur has a difficult time convincing some castle guards that he and his servant Patsy found coconuts in Mercea. So Patsy decided to build a Rails app to track his swallow friends and all of the coconuts that the birds carry around.
Migration and Models
In order to accomplish this, Patsy knew that he had to have a table for swallows (and their names) and a table for coconuts (and their weights). A swallow can carry many coconuts and a coconut can be carried by many swallows.
So, Patsy followed the Rails Guides and created a migration with three tables:
create_table :coconuts do |t|
t.float :weight
t.timestamps
endcreate_table :swallows do |t|
t.string :name
t.timestamps
endcreate_table :coconuts_swallows do |t|
t.belongs_to :coconut
t.belongs_to :swallow
t.timestamps
end
Patsy also created two models:
class Coconut < ApplicationRecord
has_and_belongs_to_many :swallows
endclass Swallow < ApplicationRecord
has_and_belongs_to_many :coconuts
end
After this, Patsy was able to open the Rails console and save his swallows and coconuts:
geoffrey = Swallow.create(name: 'Geoffrey')
heidi = Swallow.create(name: 'Heidi')
lucille = Swallow.create(name: 'Lucille')
willis = Swallow.create(name: 'Willis')c1 = Coconut.create(weight: 2.1)
c2 = Coconut.create(weight: 1.3)
c3 = Coconut.create(weight: 1.7)
c4 = Coconut.create(weight: 2.2)geoffrey.coconuts << c1heidi.coconuts << c1
heidi.coconuts << c2
heidi.coconuts << c3
heidi.coconuts << c4lucille.coconuts << c1
lucille.coconuts << c2
lucille.coconuts << c3willis.coconuts << c2
willis.coconuts << c4
Oh, Ni! A Problem.
Patsy built a page to list all of the swallows and the coconuts they carried. It ended up looking something like this:
Geoffrey
* 1: 2.1 lbsHeidi
* 1: 2.1 lbs
* 2: 1.3 lbs
* 3: 1.7 lbs
* 4: 2.2 lbsLucille
* 1: 2.1 lbs
* 2: 1.3 lbs
* 3: 1.7 lbsWillis
* 2: 1.3 lbs
* 4: 2.2 lbs
The page uses one index route for swallows GET /swallows
that includes the data for coconuts.
However, Patsy just found out that Lucille and Heidi carried Coconut 2 on their own; Willis didn’t help. So Patsy needs a way to remove a coconut-swallow association without destroying the whole coconut record!
Solution 1: Use the #delete Method
From the Rails console, Patsy can run:
willis = Swallow.find_by(name: 'Willis')
c2 = Coconut.find(2)
willis.coconuts.delete(c2)
In doing so, Patsy will not destroy the Coconut 2 record, but he will remove Coconut 2 from Willis’ coconuts collection, as per the Rails Guides documentation on the delete
method. So, Patsy can write a new controller action that specifies which coconuts need to be removed from a given swallow’s collection and then invokes this Rails code for the appropriate coconuts.
Solution 2: Use Nested Attributes and has_many, through:
However, Patsy brainstorms a little more and comes up with a solution that he likes better!
He creates a file for the join model:
class CoconutsSwallow < ApplicationRecord
belongs_to :coconut
belongs_to :swallow
end
And he updates the existing models to use has_many, through:
instead of has_and_belongs_to_many
:
class Swallow < ApplicationRecord
has_many :coconuts_swallows
has_many :coconuts, through: :coconuts_swallows
endclass Coconut < ApplicationRecord
has_many :coconuts_swallows
has_many :swallows, through: :coconuts_swallows
end
Patsy updates the controller to nest coconuts_swallows
in the records for swallows, and coconuts
within that; for example:
{
"id": 4,
"name": "Willis",
"coconuts_swallows": [
{
"id": 9,
"coconut_id": 2,
"swallow_id": 4,
"coconut": {
"id": 2,
"weight": 1.3
}
}
]
}
He then cycles back to the swallow.rb
model file, and allows it to accept nested attributes for coconuts_swallows
:
class Swallow < ApplicationRecord
has_many :coconuts_swallows
has_many :coconuts, through: :coconuts_swallows accepts_nested_attributes_for :coconuts_swallows,
allow_destroy: true
end
Since coconuts_swallows
is the join model that represents the relationship between a swallow and a coconut, destroying a CoconutsSwallow record simply means that the swallow referenced did not carry the specified coconut; it does not destroy the entire coconut (thank goodness — coconuts are delicious!)
Now, when Patsy sends information back to the server, he adds a _destroy
flag to the coconuts_swallows_attributes
parameter:
{
"id": 4,
"name": "Willis",
"coconuts_swallows_attributes": [
{
"id": 9,
"coconut_id": 2,
"swallow_id": 4,
"coconut": {
"id": 2,
"weight": 1.3
},
"_destroy": true
}
]
}
Success! Now that Patsy is a brave and successful knight-slash-Ruby-programmer, he takes a break to play with his dog Patsy Jr.