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.
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.
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 anid
.You can add attributes to
Slide
.You can use
:through
withhas_many
,belongs_to
, andhas_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.