Many-to-many relationships
Data modeling is the science (and art) of creating the database schema that most purely matches the real world objects involved in your project. Part of this is defining how the objects relate to one another. Let’s say your application tracks Items and Categories. If each item can only belong to one category, then you have a one-to-many relationship; categories have many items. But if an item can appear in more than one category, you have a many-to-many relationship.
There are two ways to handle many-to-many relationships in Ruby on Rails, and this article will cover both.
has_and_belongs_to_many
The simplest approach is if you don’t need to store any information about the relationship itself. You just want to know what items are in each category, and what categories each item belongs to. This is called “has_and_belongs_to_many”. We use has_and_belongs_to_many associations in our models, and create a join table in our database. Here are your models:
# app/models/category.rb class Category < ActiveRecord::Base has_and_belongs_to_many :items end # app/models/item.rb class Item < ActiveRecord::Base has_and_belongs_to_many :categories end
Next, let’s create the join table by generating a new migration. From the command line:
script/generate migration AddCategoriesItemsJoinTable
Now we’ll edit the migration file it creates:
class AddCategoriesItemsJoinTable < ActiveRecord::Migration
def self.up
create_table :categories_items, :id => false do |t|
t.integer :category_id
t.integer :item_id
end
end
def self.down
drop_table :categories_items
end
end
Notice the :id => false, which keeps the migration from generating a primary key. The name of the table is a combination of the two table names we’re joining, in alphabetical order. This is how Rails knows how to find the join table automatically.
has_many :through
The other way to setup a many-to-many relationship between objects is used if you do, or think you will, need to track info on the relationship itself. When was item X added to category Y? That’s info you can’t store in the category or item tables, because it’s info about the relationship. In Rails, this is called a has_many :through association, and it’s really just as easy as the first way.
First, we’re going to create a new model, that defines the relationship between items and categories. For back of a better name, let’s call it a Categorization. Setup your models like this:
# app/models/category.rb class Category < ActiveRecord::Base has_many :categorizations has_many :items, :through => :categorizations end # app/models/item.rb class Item < ActiveRecord::Base has_many :categorizations has_many :categories, :through => :categorizations end # app/models/categorization.rb class Categorization < ActiveRecord::Base belongs_to :category belongs_to :item end
We’re connecting both original models to :categorizations, and then connecting the them to each other via the intermediary Categorization model. Now, instead of a join table whose only function is connecting the others, we add a full-fledged table to manage our new model:
class CreateCategorizations < ActiveRecord::Migration
def self.up
create_table :categorizations do |t|
t.integer :category_id
t.integer :item_id
t.timestamps
end
end
def self.down
drop_table :categorizations
end
end
We still have the two foreign key integer columns, but we’ve removed :id => false so this table will have an id column of its own. We also added timestamps, so we’ll be able to tell when an item was added to a specific category. I also created a migration that removes the old categories_items table, but it’s not shown here.
Which is Better?
The simpler has_and_belongs_to_many approach has a small advantage when you *know* you’re not going to need to track info about the relationship itself. If this is the case, there’s a very slight performance gain because you’re not loading an extra model class at runtime.
More often than not, however, you’re going to eventually want to track relationship-specific data. We used the example of tracking when a relationship was created. Another would be if you want to track, over time, how many times a visitor clicks on an item under each category. That counter needs to be stored in the Categorization model, and that’s a reason not to use the simpler has_and_belongs_to_many approach.
I’ve created an example application (get it here) with tags for each version – has_and_belongs_to_many, and has_many :through.
Tags: Database, developers, habtm, has_and_belongs_to_many, has_many, primary key, Rails, Ruby, ruby on rails
March 22, 2010 at 6:00 pm |
You might probably want to add that a conversion between these two models of crafting the relationship is really not a great deal with migrations. To convert a habtm into its own model all one has to do is add a primary id to the existing table (and fill it of course), add the fields you need and then refactor some code, or am I wrong?
I just wanted to stress that this decision is not something you can’t ‘fix’ relatively easy once you have the need to add additional information to the relationship.
March 25, 2010 at 7:51 am |
@fo – you’re right. By changing the model relationships to has_many :through, and adding a primary key (as well as any other fields you now want to use) you can convert mid-stream without too much headache.
One thing to consider though is that you’ll likely have to change the way you interact with the models themselves. Depending on what new attributes you’re adding to the join table now that it’s a model unto itself, you might need to change the way you’re creating the relationships. Where you might have done
before, now you may need to convert those to
category.categorizations.create :item => item, :some_attribute => true, :other_attribute => 'i like turtles'wherever you’re creating that relationship.
Depending on the size of the app, this could be time consuming. But you’re right, it’s not the end of the world to switch midstream – in either direction, really.
August 2, 2010 at 12:52 pm |
I have a problem with many-to-many association. Please help me.
I have a Profile, a Group.
Profile(id, name, etc)
Group(id, name, etc)
and models:
Profile
- has_many :group
Group
- belongs_to :profile
(ofcourse)
Now I expand my site with an extra model GroupMember(id, group_id, member_id, etc), is model contains members of a Group, each member is a Profile.
GroupMember model
- belongs_to :group
- belongs_to :profile
How should I add more associations into Group and Profile model?
Thanks.
October 11, 2010 at 8:24 pm |
Thank you for this. Very concise and well written. Just what I was looking for.
February 9, 2011 at 6:42 am |
Gtreat post,
Could you explain as well, how would the form looks like for item and category,for has_many :through relation, considering the Rails way …
August 3, 2011 at 12:19 pm |
excellent, straightforward explanation of how to easily relate data with RoR. Thanks!
April 11, 2012 at 11:04 pm |
thanks…keep up the good work
June 27, 2012 at 1:15 pm |
really goog. thanks..
August 29, 2012 at 9:18 pm |
I just wanted to add another thank you to this. It’s been a while since you posted it and it ended up getting me past some rather hair pulling adventures in trying to figure my own way through it. Well written and spot on, thanks again.
November 20, 2012 at 9:45 pm |
thanks, very clear !