Basic many-to-many Associations in Rails


View the Source Code

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.

About these ads

Tags: , , , , , , , ,

10 Responses to “Basic many-to-many Associations in Rails”

  1. fo Says:

    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.

    • Jaime Bellmyer Says:

      @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

      category.items << item
      

      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.

  2. huetoday Says:

    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.

  3. David W Says:

    Thank you for this. Very concise and well written. Just what I was looking for.

  4. Wadziu Says:

    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 …

  5. Kevin Causey Says:

    excellent, straightforward explanation of how to easily relate data with RoR. Thanks!

  6. Mili Says:

    thanks…keep up the good work

  7. Nando Sousa Says:

    really goog. thanks..

  8. Hunter Jansen Says:

    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.

  9. Guillaume Galuz (@guillaumegaluz) Says:

    thanks, very clear !

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s


Follow

Get every new post delivered to your Inbox.

%d bloggers like this: