Examining Many-To-Many Relationships In Rails: A Quest

Amanda Chang
Building RigUp
Published in
4 min readDec 3, 2019

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.

Swallows and a Coconut
Image by Larry Wentzel

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
end
create_table :swallows do |t|
t.string :name
t.timestamps
end
create_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
end
class 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 << c4
lucille.coconuts << c1
lucille.coconuts << c2
lucille.coconuts << c3
willis.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 lbs
Heidi
* 1: 2.1 lbs
* 2: 1.3 lbs
* 3: 1.7 lbs
* 4: 2.2 lbs
Lucille
* 1: 2.1 lbs
* 2: 1.3 lbs
* 3: 1.7 lbs
Willis
* 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.

Image of King Arthur and Patsy

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
end
class 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.

--

--