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.