Understanding Many To Many Association in Rails

Rails make it possible to handle many to many associations with the has_and_belongs_to_many and has_many through. Before going into the details on them, its important to be sure if a many to many association is what you really need. A many to many relationship is created when a TableA can have multiple occurrences of TableB and TableB can have multiple occurrences of table A. A good example is how projects can be distributed across employees in a workplace. An employee can have multiple projects and a project can be assigned to multiple employees. So if there's a project almanac in the company and Bob and Juliet are working on several other company projects, they could both be assigned to this project while still working on their other projects.

HABTM

The HABTM (Has and Belongs To Many Table) is sufficient when you aren't storing any metadata on the relationships. It uses a JOIN table to store the foreign keys between both tables. For these examples I'll use an employee model and a product model. The first step will be to create the migrations.
For employees,

$rails g migration create_employees name:string position:string

which should generate this:

class CreateEmployees < ActiveRecord::Migration  
  def change
    create_table :employees do |t|
      t.string :name
      t.string :position
      t.timestamps null: false
    end
  end
end  

For product, generate the migration and you should have this:

class CreateProducts < ActiveRecord::Migration  
  def change
    create_table :products do |t|
      t.string :name
      t.string :description
      t.timestamps null: false
    end
  end
end  

Then the JOIN table

$rails g migration create_employees_products

Pay attention to the table name. It has to have both tables pluralized and in an alphabetic order separated with underscore. Now modify your JOIN table migration as follows

class CreateEmployeesProducts < ActiveRecord::Migration  
  def change
    create_table :employees_products, id: false do |t|
      t.integer :employee_id
      t.integer :product_id
    end
    add_index :employees_products, :employee_id
    add_index :employees_products, :product_id
  end
end  

The :id => false on create_table is to make sure we don't have any primary keys on the table. It's simply just a JOIN table that takes ids of both associated models. I also added index to speed up queries.
If you think it's a lot of work to go through this you can use the HABTM generator gem
The next step will be to create the association in the employee and product model

class Employee < ActiveRecord::Base  
  has_and_belongs_to_many :products
end  
class Product < ActiveRecord::Base  
  has_and_belongs_to_many :employees
end  

With all that set you can now fetch records within each of them with simple AR queries like:

# Fetching products that belong to an employee
e = Employee.find(1)  
e.products  
# Creating products for an employee
e.products.create(name: 'project almanac')  
HABTM Checkboxes

When creating the views for our associated tables. A form for adding employees may require that you select the number of products that will be attached to each employee. We need to have checkboxes to select from the existing products and if you're wondering how to have that get properly updated in your join table, it's easy with the rails power. For a simple form object for employee you can have a new controller method:

def new  
  @employee = Employee.new
  @products = Product.all
end  

and in your view you would have:

<%= form_for @employee do |f| %>  
<%= f.collection_check_boxes :product_ids, @products, :id, :name %>  
<% end %>  

and that produces check boxes for all the products. There are more methods that can be used with the has_many_and_belongs_to be sure to check out the guides for more.

HMT

The HMT (Has Many Through) uses a join table that can store extra data about the relationship between the two models. Imagine we need to keep tasks info for the employees on each product. We will have a JOIN table for tasks with a migration like this:

class CreateTasks < ActiveRecord::Migration  
  def change
    create_table :tasks do |t|
      t.belongs_to :employee, index: true
      t.belongs_to :product, index: true
      t.string :task_description
      t.datetime :task_assign_date
      t.timestamps null: false
    end
  end
end  

Then models for employee, product, and task can be written as:

class Employee < ActiveRecord::Base  
  has_many :tasks
  has_many :products, through: :tasks
end  
class Product < ActiveRecord::Base  
  has_many :tasks
  has_may  :employees, through: :tasks
end  
class Task < ActiveRecord::Base  
  belongs_to :employee
  belongs_to :product
end  

I find has_many_and_belongs_to useful for most of my use-cases but I understand when it's important to use has_many through. It's common to see developers that choose to always use HMT when HABTM is what's really meant for the job.

Joseph Rex

Trainer of dragons.

Subscribe to Mobnia

Get the latest posts delivered right to your inbox.

or subscribe via RSS with Feedly!