Examining Many-To-Many Relationships In Rails: A Quest
By: Amanda Chang
Tags: Ruby on Rails

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.

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 << c1 heidi.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.

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.