Posts Tagged ‘has_many’

Basic many-to-many Associations in Rails

January 29, 2010

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.

Nesting your has_many :through relationships

January 28, 2010

View the Source Code

Let’s say you’re creating a site where people can track their memberships in various store clubs – from grocery store loyalty cards, to memberships to Sam’s or CostCo. People and Stores have a many-to-many relationship called a Membership. Stores also have sales, and you want people to be able to manage all sales at all of their stores easily.

The Problem

You’d like to be able to say @member.sales, but there’s a problem – Rails doesn’t support daisy-chaining associations the way we’d like. Here’s how we want to setup our associations:

# app/models/member.rb
class Member < ActiveRecord::Base
  has_many :memberships
  has_many :clubs, :through => :memberships
  has_many :sales, :through => :clubs
end
# app/models/club.rb
class Club < ActiveRecord::Base
  has_many :memberships
  has_many :members, :through => :memberships
  
  has_many :sales
end
# app/models/membership.rb
class Membership < ActiveRecord::Base
  belongs_to :member
  belongs_to :club
end
# app/models/sale.rb
class Sale < ActiveRecord::Base
  belongs_to :club
end

And for reference, here’s the full schema:

# db/schema.rb
ActiveRecord::Schema.define(:version => 20100129152803) do
  create_table "clubs", :force => true do |t|
    t.string   "name"
    t.datetime "created_at"
    t.datetime "updated_at"
  end

  create_table "members", :force => true do |t|
    t.string   "name"
    t.datetime "created_at"
    t.datetime "updated_at"
  end

  create_table "memberships", :force => true do |t|
    t.integer  "member_id"
    t.integer  "club_id"
    t.date     "expires"
    t.datetime "created_at"
    t.datetime "updated_at"
  end

  create_table "sales", :force => true do |t|
    t.string   "name"
    t.text     "description"
    t.date     "start"
    t.date     "end"
    t.integer  "club_id"
    t.datetime "created_at"
    t.datetime "updated_at"
  end
end

While everything looks good from a data modeling perspective, there’s one issue – our member model isn’t allowed to daisy-chain assocations, so we can’t get to our sales easily. A call to @member.sales gives us this:

ActiveRecord::StatementInvalid: SQLite3::SQLException: no such column: clubs.member_id: SELECT "sales".* FROM "sales"  INNER JOIN "clubs" ON "sales".club_id = "clubs".id    WHERE (("clubs".member_id = 1)) 

The Solution

In comes Ian White’s nested_has_many_through plugin, which does exactly what you’d think Rails does already. Without changing the way you create associations, this plugin “just works” out of the box. Nothing to include in models or config files. Here’s how you install it:

script/plugin install git://github.com/ianwhite/nested_has_many_through.git

Now run @member.sales and you get what you’d expect – a list of all sales that a member is entitled to attend. You can nest even deeper if you like, but I offer this word of caution. Nested has_many :through associations are like the most precious liquid on earth: Captain Morgan’s spiced rum. Enjoy in moderation :)

Epilogue

I’ve created a full rails app on GitHub (View the Source Code) so you can download and play around with it. There are “before” and “after” tags, so you can see how the app reacts with and without the plugin. It also has Shoulda tests.

Many-to-Many Relationships in Ruby on Rails

January 16, 2010

Many-to-many relationships in Rails can be confusing at first. There are a couple different ways to handle them, depending on what you need. Let’s say you want to organize your mp3 collection. Artists obviously have multiple songs, but songs also have multiple artists.

has_and_belongs_to_many

The simpler way to do this is using HABTM (has_and_belongs_to_many) to define your relationships. Here are the models:

# app/models/song.rb
class Song &lt; ActiveRecord::Base
  has_and_belongs_to_many :artists
end

# app/models/artist.rb
class Artist &lt; ActiveRecord::Base
  has_and_belongs_to_many :songs
end

And here are the migrations:

# db/migrate/create_artists.rb
class CreateArtists &lt; ActiveRecord::Migration
  def self.up
    create_table :artists do |t|
      t.string :name
      t.timestamps
    end
  end

  def self.down
    drop_table :artists
  end
end

# db/migrate/create_songs.rb
class CreateSongs &lt; ActiveRecord::Migration
  def self.up
    create_table :songs do |t|
      t.string :name
      t.timestamps
    end
  end

  def self.down
    drop_table :songs
  end
end

# db/migrate/create_artists_songs.rb
class CreateArtistsSongs &lt; ActiveRecord::Migration
  def self.up
    create_table :artists_songs, :id =&gt; false do |t|
      t.integer :artist_id
      t.integer :song_id
    end
  end

  def self.down
    drop_table :artists_songs
  end
end

Notice a couple things about the migrations. First, I had to manually create a join table migration. Its name must be a combination of the two tables being joined, in alphabetical order. This is how Rails is able to figure out the join table’s name automatically. Also notice that I had to specify :id => false in the join table definition. Primary keys in join tables cause big problems.

Now, if I have an object song that is an instance of the Song class, I can say song.artists and get a list of the artists associated with this song. I can do the same for artists. Yay!

has_many :through

What if we wanted to track what instrument the artist played on a given song? This doesn’t go in the artists table, because it’s different for every song. It doesn’t go in the songs table either, because each artist probably played something different. It needs to go in the join table, but there’s a problem. There’s no way in your Rails code to access the information stored in the join table itself.

What we really have here is more than just a join table – it’s another model all its own. Let’s call it a contribution. Each artist’s contribution to a song is unique. I might play nose flute in a rendition of Herbie Hancock’s Watermelon Man, but I play the slide whistle when recreating the theme song from the Krusty the Klown show.

Recreating the models from scratch looks like this:

# app/models/artist.rb
class Artist  &lt; ActiveRecord::Base
  has_many :contributions
  has_many :songs, :through =&gt; :contributions
end

# app/models/contribution.rb
class Contribution &lt; ActiveRecord::Base
  belongs_to :artist
  belongs_to :song
end

# app/models/song.rb
class Song &lt; ActiveRecord::Base
  has_many :contributions
  has_many :artists, :through =&gt; :contributions
end

The migrations look like this:

# db/migrate/create_artists.rb
class CreateArtists &lt; ActiveRecord::Migration
  def self.up
    create_table :artists do |t|
      t.string :name
      t.timestamps
    end
  end

  def self.down
    drop_table :artists
  end
end

# db/migrate/create_contributions.rb
class CreateContributions &lt; ActiveRecord::Migration
  def self.up
    create_table :contributions do |t|
      t.references :artist
      t.references :song
      t.string :instrument
      t.timestamps
    end
  end

  def self.down
    drop_table :contributions
  end
end

# db/migrate/create_songs.rb
class CreateSongs &lt; ActiveRecord::Migration
  def self.up
    create_table :songs do |t|
      t.string :name
      t.timestamps
    end
  end

  def self.down
    drop_table :songs
  end
end

I didn’t have to manually create the join table this time. I created the three models calling script/generate, and the tables were created automatically. Now, with an object artist that is an instance of the Artist class, I can get a list of contributions with artist.contributions. I can get a list of songs just as easily with artist.songs, and Rails will find them through the contributions table. If I want the instrument the artist used on the first song, I can say artist.songs[0].contribution.instrument, and so on.

Deciding Which to Use

If two models have a many-to-many relationship and you need to track info about the relationship itself, use has_many :through. In fact, sometimes it’s wise to do this anyway, if you anticipate needing that later. HABTM should only be used when the relationship itself will never need to track data of its own.


Follow

Get every new post delivered to your Inbox.