has_and_belongs_to_many

Many-to-many relationships are more complex than the three relationships shown so far because these relationships require an additional table in the database. Rather than relying on a single foreign key column, you’ll need a relationship table, also called a join table. Each row of a join table expresses a relationship with foreign keys, but has no other data. Figure 4-4 shows our relationship table.

A has_and_belongs_to_many association builds a many-to-many relationship through a join table

Figure 4-4. A has_and_belongs_to_many association builds a many-to-many relationship through a join table

Photo Share requires a many-to-many relationship between Photo and Category. A category can hold many photos, and the same photo can fit into more than one category. Many-to-many relationships won’t work with a single foreign key. You already have a working model for both Category and Photo, but this many-to-many relationship will require a join table to manage relationships between the two classes. By convention, the join table should have the foreign keys photo_id and category_id. Generate that table now:

$ script/generate migration create_categories_photos
      exists  db/migrate
      create  db/migrate/20080511031501_create_categories_photos.rb

The generator created a migration for you, but not a model. You don’t need an additional model for a join table. The Active Record naming convention for the relationship table is classes1_classes2, with the classes in alphabetical order. Edit the migration in db/migrate/20080511031501_create_categories_photos.rb to look like this:

class CreateCategoriesPhotos < ActiveRecord::Migration
  def self.up
    create_table :categories_photos, :id => false do |t|
      t.integer   :category_id
      t.integer   :photo_id
    end
  end

  def self.down
    drop_table :categories_photos
  end
end

Run the migration with rake db:migrate. Note that the categories_photos statement has the option :id => false. We added this option because the database table needs no id, only the foreign keys to photos and categories. The app/models/photo.rb and app/models/category.rb models need the has_and_belongs_to_many relationship macro, like this:

class Photo < ActiveRecord::Base

...
  has_many :slides
  has_and_belongs_to_many :categories
...

Next, edit app/models/category.rb to look like this:

class Category < ActiveRecord::Base
  has_and_belongs_to_many :photos
end

The has_and_belongs_to_many macro works just like the other Active Record macros you’ve seen. It will add the appropriate attributes and constructors to the each class. To play with the class, you will need some test data for the categories. Create a fixture in test/fixtures/categories_photos.yml that looks like this:

<% 1.upto(9) do |i| %>
categories_photos_<%= i %>: 
  category_id: 1
  photo_id: <%= i %>
<% end %>

Load your fixtures with rake db:fixtures:load, or run rake photos:reset to run migrations and load your test data. Now, you can see how categories are working inside the console with a richer set of data:

>> Photo.find(1).categories
=> [#<Category id: 1, parent_id: 1, name: "All",
created_at: "2008-05-11 00:37:27", updated_at: "2008-05-11 00:37:27">]
>> Category.count
=> 7
>> Category.find(1).photos.collect {|photo| photo.filename}.join ', '
=> "gargoyle.jpg, cat.jpg, cappucino.jpg, building.jpg, bridge.jpg,
bear.jpg, baskets.jpg, train.jpg, lighthouse.jpg"
>> all = Category.find(:first)
=> #<Category id: 1, parent_id: 1, name: "All", 
created_at: "2008-05-11 00:37:27", updated_at: "2008-05-11 00:37:27">

As expected, you get an array called photos on category that’s filled with photos associated in the join table categories_photos. Let’s add a photo:

>> chunky_bacon = Photo.new(:filename => 'chunky_bacon.jpg')
=> #<Photo id: nil, filename: "chunky_bacon.jpg", thumbnail: nil, description: nil, 
created_at: nil, updated_at: nil>
>> chunky_bacon.id
=> nil
>> all.photos << chunky_bacon
=> [#<Photo id: 3, filename: "gargoyle.jpg", ...]
>> chunky_bacon.id
=> 10
>> chunky_bacon.new_record? 
=> false

Take a look at this statement: all.photos << chunky_bacon. (It adds a photo to all.photos.) You can see that the << operator adds an object to a collection and saves the collection. Because the initial chunky_bacon.id statement returns nil, you know that the object has not yet been saved. The << operator adds an object to a collection. As you know, the collection is represented in the database as two ids: one for the photo and one for the category. Active Record must save the record before adding it to a category to get the id. The behavior is a little jarring if you’re not ready for it.

The methods and attributes added by the has_and_belongs_to_many method are identical to those added by has_many. They were shown in Table 4-2.

Join Models

Sometimes, it’s useful to be able to add columns to a relationship table. You might wonder whether it’s possible to create a Rails model from the categories_photos table. You can’t do so with the has_and_belongs_to_many macro in its basic form—you need join models and the through option. For example, we could have easily decided that a slide was just a join table between photo and slideshow with an attribute parameter. We could have expressed that relationship in this way:

class Slideshow < ActiveRecord::Base
  has_many :photos :through => :slides
end

This example relies on tables and models for photos, slideshows, and slides. The join table is a first-class model, but also serves as a relationship table. The structure in the example is slightly different from a typical join table. The primary differences are these:

  • The Slide is a first-class model with an id.

  • You can add attributes to Slide.

  • You can use :through with has_many, belongs_to, and has_and_belongs_to_many.

The :through relationship makes it possible to build much more sophisticated relationships, allowing you to identify and tag each relationship with additional data as required.

Get Rails: Up and Running, 2nd Edition now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.