Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

When to use a "has_many :through" relation in Rails?

Say you have these models:

Car
Engine
Piston

A car has_one :engine
An engine belongs_to :car
An engine has_many :pistons
Piston belongs_to :engine

A car has_many :pistons, through: :engine
Piston has_one :car, through: :engine

Essentially you are delegating a model relationship to another model, so instead of having to call car.engine.pistons, you can just do car.pistons


Say you have two models: User and Group.

If you wanted to have users belong to groups, then you could do something like this:

class Group < ActiveRecord::Base
  has_many :users
end

class User < ActiveRecord::Base
  belongs_to :group
end

What if you wanted to track additional metadata around the association? For example, when the user joined the group, or perhaps what the user's role is in the group?

This is where you make the association a first class object:

class GroupMembership < ActiveRecord::Base
  belongs_to :user
  belongs_to :group

  # has attributes for date_joined and role
end

This introduces a new table, and eliminates the group_id column from the user's table.

The problem with this code is that you'd have to update every where else you use the user class and change it:

user.groups.first.name

# becomes

user.group_memberships.first.group.name

This type of code sucks, and it makes introducing changes like this painful.

has_many :through gives you the best of both worlds:

class User < ActiveRecord::Base
  has_many :group_memberships
  has_many :groups, :through => :group_memberships  # Edit :needs to be plural same as the has_many relationship   
end

Now you can treat it like a normal has_many, but get the benefit of the association model when you need it.

Note that you can also do this with has_one.

Edit: Making it easy to add a user to a group

def add_group(group, role = "member")
  self.group_associations.build(:group => group, :role => role)
end

ActiveRecord Join Tables

has_many :through and has_and_belongs_to_many relationships function through a join table, which is an intermediate table that represents the relationship between other tables. Unlike a JOIN query, data is actually stored in a table.

Practical Differences

With has_and_belongs_to_many, you don't need a primary key, and you access the records through ActiveRecord relations rather than through an ActiveRecord model. You usually use HABTM when you want to link two models with a many-to-many relationship.

You use a has_many :through relationship when you want to interact with the join table as a Rails model, complete with primary keys and the ability to add custom columns to the joined data. The latter is particularly important for data that is relevant to the joined rows, but doesn't really belong to the related models--for example, storing a calculated value derived from the fields in the joined row.

See Also

In A Guide to Active Record Associations, the recommendation reads:

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).

You should use has_many :through if you need validations, callbacks, or extra attributes on the join model.