Understanding has_many :through MyTravels

Posted by rhhaynes on May 18, 2018

For my Rails portfolio project I decided to create a content management system called MyTravels for making, organizing, and documenting travel plans. Based on user experiences, the application provides insight into the world’s most popular destinations and the features that make them unique. Without going into detail, after creating an account users have the ability to make new travel plans. For existing plans users can submit journal entries or notes in the form of travel logs to detail their upcoming itineraries or past experiences.

MyTravels table relations

Ignoring travel log functionality for now, behind the scenes the MyTravels application is built upon three Active Record models with the following associations.

User
has_many :travels
has_many :destinations, :through => :travels
Destination
has_many :travels
has_many :users, :through => :travels
Travel
belongs_to :user
belongs_to :destination

Given this setup it should be apparent that a many-to-many relationship exists between the User and Destination models. But why did we choose to define this relationship using a has_many :through approach which requires a separate join model (i.e., Travel)? Especially when Rails offers a second and much simpler approach, has_and_belongs_to_many, which only relies on the join table?

Declaring many-to-many relationships

To understand this decision we need only reference the helpful Rails Guide which states:

The simplest rule of thumb is that you should set up a has_many :through relationship if you need to work with the relationship model as an independent entity. If you don’t need to do anything with the relationship model, it may be simpler to set up a has_and_belongs_to_many relationship (though you’ll need to remember to create the joining table in the database).

So what does this mean? Well in the context of MyTravels it means that we should use a has_many :through approach if the relationship model, in our case Travel, requires its own data or attributes. And as it turns out, this is exactly our situation.

Defining the join model

In the real world when one wishes to describe travel, it usually involves answering the following questions.

  • Where are you going?
  • Why are you going?
  • When are you leaving?
  • How long will you be there?

And because we’ve defined a Destination model to handle the where aspect of traveling, the question of which many-to-many relationship to use really comes down to whose responsibility is it to address the other questions. Since it doesn’t make sense to assign travel dates or purposes to a User, a relationship model acting as an independent entity works perfectly here. By setting up a has_many :through relationship and creating a Travel model, we now have a very intuitive place to define travel attributes such as purpose, start date, and end date.

It should be noted that even if we could reasonably handle the why, when, and for how long questions with our User model — thus allowing us to leverage the simpler has_and_belongs_to_many approach — most experienced developers would still recommend using the has_many :through relationship. Why? Because it’s hard to be 100% certain that you’ll never need to work with join data over the lifetime of a project.

Safe travels

In summary one should only use the has_and_belongs_to_many approach when they are absolutely certain the join data in a many-to-many association will serve no purpose… almost never. So if you’re looking for a golden rule on the topic, stick with has_many :through for defining all many-to-many relationships. Happy coding and safe travels!