O'Reilly logo

Rails: Up and Running, 2nd Edition by Curt Hibbs, Lance Carlson, Bruce Tate

Stay ahead with the world's most comprehensive technology and business learning platform.

With Safari, you learn the way you learn best. Get unlimited access to videos, live online training, learning paths, books, tutorials, and more.

Start Free Trial

No credit card required

Chapter 4. Active Record Relationships

You’ve seen how Active Record treats a model backed by a single table. You wrote very little code, and got impressive behavior for free. But Photo Share will need many different models working together. In this chapter, you’ll learn how to deal with relationships. Active Record takes advantage of the Ruby language and naming conventions to simplify the way you work with related tables through related models.

In Chapter 3, you got a taste of Active Record’s language for relationships and validations. Adding the macro validates_presence_of :filename record to the class added all of the code that your class needed to support validation. You’ll deal with relationships the same way. Rather than adding a bunch of methods and attributes to your class explicitly to manage a foreign key relationship, you’ll describe the relationship using a little bit of syntax called a macro and let Active Record do the rest. Rails will add everything you need to manage each relationship based on a few short lines of code.

You’ll specify a relationship, also called an association, in three parts: the relationship macro, the association or target, and an optional hash of additional parameters. For example, this code:

class Slideshow < ActiveRecord::Base
  has_many :slides, :order => 'position'

specifies that a single Slideshow has_many :slides, and they are ordered by the position attribute. The macro is has_many, the association is :slides, and the hash of options is {:order => 'position'}.

Let’s add relationships to the existing Photo, Slide, Slideshow, and Category classes. If you haven’t already done so, and you want to start coding your way through the book with us, copy your code from http://www.oreilly.com/catalog/9780596522001, using the file for Chapter 3. Run rake photos:reset to run your migrations and reset your test data. Now you’re ready to follow along.

belongs_to

The most common database relationship is many-to-one. Figure 4-1 shows how Active Record maps the “many” side of such a relationship. In Photo Share, we want users to be able to build slideshows. A slideshow contains an ordered list of pictures. We can’t simply use pictures in a slideshow because a picture has no way of keeping its position in a slideshow, so we’ll introduce a Slide class. We’ll then need a many-to-one relationship between slides and slideshows because a slideshow consists of many slides, but each slide (a photo combined with a position) can belong to only one slideshow. A slide also belongs to a photo. We’ll implement both relationships with belongs_to.

belongs_to :association relationship between entity (slide) and association (slideshow)
Figure 4-1. belongs_to :association relationship between entity (slide) and association (slideshow)

You’ve already generated a model and migration for Slide, and another for Slideshow. You can open up db/schema.rb to verify that your Photos schema has a table called slides and another called photos. Notice that the slide has a slideshow_id and a photo_id. Any model that uses belongs_to must be backed by a table that has the supporting foreign key. Next, modify the Slide model in app/models/slide.rb, adding the two relationships:

class Slide < ActiveRecord::Base
  belongs_to :slideshow
  belongs_to :photo
end

That’s it. You have two working belongs_to relationships. Verify them briefly in the console. (Remember, you can start the console with ruby script/console, or simply use reload! to reload it whenever your models change.)

>> slide = Slide.new
=> #<Slide id: nil, position: nil, photo_id: nil, slideshow_id: nil, 
created_at: nil, updated_at: nil>
>> slide.create_slideshow
=> #<Slideshow id: 2, name: nil, created_at: "2008-05-10 17:45:46", 
updated_at: "2008-05-10 17:45:46">
>> slide.photo = Photo.find(2)
=> #<Photo id: 2, filename: "lighthouse.jpg", thumbnail: "lighthouse_t.jpg", 
description: "My ride to work", created_at: "2008-05-21 02:07:29", 
updated_at: "2008-05-21 02:07:29">
>> slide.photo.filename
=> "lighthouse.jpg"
>> slide.slideshow.id
=> 2

You can see the belongs_to relationship at work. You have at least two new attributes: slideshow and photo. You also have the create_slideshow and create_photo methods. These methods are only the tip of the iceberg. Table 4-1 shows all of the methods and attributes introduced by the belongs_to macro.

Table 4-1. Metaprogramming for belongs_to and has_one

Added Feature

Description

Methods

<association>.nil?

Test the association for a nil value: slide.photo.nil?

build_<association>

Build an object of the associated type, but do not save it yet: slide.build_photo(:filename => "cat.jpg").

In this example, slide.photo is initialized to a new unsaved photo with the specified attributes.

create_<association>

Create and save an object of the associated type, initialized to the root object. It takes a hash map of attributes for the new object as a parameter: slideshow.create_slide({...}).

Attributes

<association>

An attribute of the type of the associated object: belongs_to :photo on Slide allows slide.photo and slide.photo = nil

As you can see, just learning the macros isn’t enough. To learn about using Active Record to the fullest, you need to understand all of the methods and attributes that belongs_to creates for you. You’ll find the association attribute and the constructors—all forms of build and create methods—particularly useful. We’ll show you tables such as Table 4-1 for each macro we cover.

In the meantime, let’s take another look at the new models in the Rails console:

>> slide = Slide.find 1
=> #<Slide id: 1, position: 1, photo_id: 1, slideshow_id: 1, 
created_at: "2008-05-10 17:45:34", 
updated_at: "2008-05-10 17:45:34">
>> slide.photo.filename
=> "train.jpg"
>> slide.slideshow.name
=> "Interesting Pictures"

belongs_to is only the “many” end of a many-to-one relationship. Let’s look at the “one” side.

has_many

Now that you’ve finished the relationships on Slide, you’ll need to implement has_many relationships on Photo and Slideshow. Figure 4-2 shows the mapping between Active Record objects and database tables with has_many.

The entity (slideshow) has_many :associations (slides) relationship is a one-to-many relationship
Figure 4-2. The entity (slideshow) has_many :associations (slides) relationship is a one-to-many relationship

has_many is the other side of a belongs_to relationship. Both models and the schema exist, so you don’t need to modify the class or table for Slide. You can merely add the relationship has_many to slideshow.rb in app/models/slideshow.rb:

class Slideshow < ActiveRecord::Base
  has_many :slides
end

You’ll do the same to app/models/photo.rb:

class Photo < ActiveRecord::Base
...             
  has_many :slides
  validates_presence_of :filename
...
end

We should explain a little bit about the model. A slide belongs to a photo, but a photo has many slides. With this design, you give users the ability to use the same photo in several different slideshows. Remember: a slide is a photo and a position in a specific slideshow. So, a slide can’t be reused, but a photo can.

With those code changes, you can see all of the slides associated with a photo and all of the slides in a slideshow. As usual, you can open the console to see the model in action:

>> reload!
Reloading...
=> true
>> slide = Slide.find 1
=> #<Slide id: 1, position: 1, photo_id: 1, slideshow_id: 1, 
created_at: "2008-05-10 17:45:34", updated_at: "2008-05-10 17:45:34">
>> slideshow = slide.slideshow
=> #<Slideshow id: 1, name: "Interesting Pictures", created_at: "2008-05-10 17:45:34", 
updated_at: "2008-05-10 17:45:34">
>> slideshow.slides.each {|s| puts s.photo.filename}
train.jpg
lighthouse.jpg
gargoyle.jpg
cat.jpg
cappucino.jpg
building.jpg
bridge.jpg
bear.jpg
baskets.jpg
=> [#<Slide id: 1, ...]

So, you get a list of slides in the slideshow, and each has an associated photo. Active Record is now managing the has_many relationship between Slideshow and Slide. You could use photo.slides in the same way. Table 4-2 shows you the metaprogramming for has_many.

Table 4-2. Metaprogramming for has_many

Added feature

Description

Methods

<associations> << object

Adds an object to the <associations> collection: photo.slides << a_slide

<associations>.delete object

Deletes an object in the <associations> collection. The objects will be destroyed if the dependent parameter of has_many is set to true: photo.slides.delete a_slide

<associations>_singular_ids collection

Replaces the <associations> collection with a collection of objects identified by ids in the collection: photo.slides_singular_ids [1, 2, 3, 4]

<associations>.find

Uses the same rules as a basic find, but operates only on the items in the <associations> collection: photo.slides.find_by_position 4

<associations>.clear

Deletes all of the objects in the association: photo.slides.clear

<associations>.empty?

Tests to see if <associations> collection is empty: photo.slides.clear

<associations>.size

Returns the number of items in the <associations> collection: photo.slides.size

<associations>.build

Builds an object of the associated type without saving it. It takes a hash map of attributes for the new object as a parameter: slide.build_photo(:filename => "cat.jpg")

<associations>.create

Creates and save an object of the associated type, initialized to the root object. It takes a hash map of attributes for the new object as a parameter: slide.build_photo(:filename => "cat.jpg")

In this example, photo.slide is initialized to slide.

Attributes

<associations>

A collection of the associated objects: slide.photos[4]

has_one

The simplest database relationship is the one-to-one relationship. With Active Record, you can implement one-to-one relationships with belongs_to or has_one. You’ll decide which to use based on where the foreign key lands. For example, if an Address belonged to a Person, the addresses table would need a person_id column. Person would use a has_one relationship. Figure 4-3 shows a has_one relationship.

In this one-to-one relationship, a Photo has_one File
Figure 4-3. In this one-to-one relationship, a Photo has_one File

Let’s take a simple example. Hypothetically, you could have decided to implement photos and files in separate tables. If you put a foreign key called photo_id into the files table, you would have this Active Record Photo class:

class Photo < ActiveRecord::Base
  has_one :file
  ...
end

has_one is identical to belongs_to with respect to metaprogramming. For example, adding has_one :photo or belongs_to :photo to Slide would add the photo attribute to Slide. But the Photo Share application does not need a separate table to manage files, so let’s move on to that marathon of typing, has_and_belongs_to_many.

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.

acts_as_list

Active Record has three special relationships that let you explicitly model lists, trees, and nested sets: acts_as_list, acts_as_tree, and acts_as_nested_set, respectively. We’ll look at the two relationships required by Photo Share in this chapter: acts_as_list and acts_as_tree. acts_as_list lets you express items as an ordered list and also provides methods to move items around in the hierarchy. Figure 4-5 shows the mapping. In Photo Share, we’ll use acts_as_list to model a slideshow, which is an ordered list of slides. Later, we’ll use acts_as_tree to manage our nested categories.

acts_as_list allows an explicit ordering
Figure 4-5. acts_as_list allows an explicit ordering

First, let’s modify the existing app/models/slide.rb model. We want users to be able to move slides up and down in a show, so the slideshow needs an ordering. We’ll use the existing slides and add the Active Record macro acts_as_list.

class Slide < ActiveRecord::Base
  belongs_to :slideshow
  belongs_to :photo

  acts_as_list :scope => "slideshow_id"
end

This example builds a list of slides that compose a slideshow. belongs_to is a one-to-many relationship, imposing structure. The Slide model has a belongs_to relationship with Slideshow and Photo as the targets. acts_as_list is a helper macro, imposing order and introducing behavior related to a list. As of Rails 2.0, the macro is a plug-in and not part of the base library. To get it, type: script/plugin install acts_as_list.

$ script/plugin install acts_as_list
+ ./README
+ ./init.rb
+ ./lib/active_record/acts/list.rb
+ ./test/list_test.rb

That command loads the acts_as_list plug-in into the vendor/plugins/acts_as_list directory. From that point, you can use it just as if it were a native Rails macro. To Active Record, each macro is independent. You use the :scope parameter to tell Active Record which items belong in the list. In this case, we set the :scope parameter to :slideshow_id so all slides with the same slideshow_id will act as one independent list.

To capture ordering, Active Record uses a position attribute by default. Because you have a position column in the database, you don’t need to do anything more to the slides to support this list. However, for convenience, you’ll want the array of slides to be fetched and displayed in the right order, so make one small change to app/models/slideshow.rb:

class Slideshow < ActiveRecord::Base
  has_many :slides, :order => :position
end

We’re ready to use the list. You can use methods added by acts_as_list to change the order of slides in the slideshow, and to indicate which items are first and last:

>> show = Slideshow.find 1
=> #<Slideshow id: 1, name: "Interesting Pictures", created_at: "2008-05-11 00:37:27", 
updated_at: "2008-05-11 00:37:27">
>> show.slides.each {|slide| puts slide.photo.filename}
train.jpg
lighthouse.jpg
gargoyle.jpg
cat.jpg
cappucino.jpg
building.jpg
bridge.jpg
bear.jpg
baskets.jpg
=> [#<Slide id: 1, ...]
>> show.slides.first.photo.filename
=> "train.jpg"
>> show.slides.first.move_to_bottom
=> true
>> show.slides.last.photo.filename
=> "baskets.jpg"
>> show.reload
=> #<Slideshow id: 1, name: "Interesting Pictures", created_at: "2008-05-21 02:19:32", 
updated_at: "2008-05-21 02:19:32">
>> show.slides
=> [#<Slide id: 2, ...>, ...]
>> show.slides.last.photo.filename
=> "train.jpg"

By convention, positions start at 1 and are sequentially numbered through the end of the list. Position 1 is the top, and the biggest number is the bottom. You can move any item higher or lower, move items to the top or bottom, create items in any position, and get relative items in the list, as in Table 4-3. Keep in mind that moving something higher means making the position smaller, so you should think of the position as a priority. Higher positions mean higher priorities, so they’ll be closer to the front of the list.

Table 4-3 shows all the methods added by the acts_as_list relationship. Keep in mind that you’ll use acts_as_list on objects that already have a belongs_to relationship, so you’ll also get the methods and attributes provided by belongs_to. You’ll also inherit the methods from the array, so slideshow.slides[1] and slideshow.slides.first are both legal.

Table 4-3. Metaprogramming features for acts_as_list

Added feature—methods

Description

increment_position

Increments the position attribute of this list element:

slideshow.slides[1].increment_position

decrement_position

Decrements the position attribute of this list element:

slideshow.slides[2].decrement_position

higher_item

Returns the previous item in the list. Higher means closer to the front, or closer to index 1, as in priority:

slideshow.slides[2].higher_item

lower_item

Returns the next item in the list. Lower means closer to the back, or farther from index 1, as in priority:

slideshow.slides[1].lower_item

in_list?

Tests whether an object has been added to a list:

slide.in_list?

insert_at position

Inserts the current item at a given position. Default is position 1:

slide.insert_at(1)

first?

Returns true if position==1; false otherwise:

slide.first?

last?

Returns true if position is the largest in the list; return false otherwise:

slideshow.slides[7].last?

move_higher

Moves this item toward index 1:

slideshow.slides[4].move_lower

move_lower

Moves this item away from index 1:

slideshow.slides[3].move_higher

move_to_top

Moves this item to index 1:

slideshow.slides[3].move_to_top

move_to_bottom

Makes this item the last in the list:

slideshow.slides[3].move_to_bottom

remove_from_list

Removes this item from the list:

slideshow.slides[3].remove_from_list

Trees

Let’s think about the most complex relationship in Photo Share: nested categories. You could implement categories by adding belongs_to :category and has_many :categories to the Category class. The resulting code would be awkward because a category would have an attribute called category (for the parent) and another called categories for the children. parent and children attributes would be better, but you’d be forced to override Active Record naming conventions and to write much more code.

This arrangement is common enough that Active Record has the acts_as_tree relationship, shown in Figure 4-6. Just as you did with acts_as_list, you will need to install a plug-in.

$ script/plugin install acts_as_tree
+ ./README
+ ./Rakefile
+ ./init.rb
+ ./lib/active_record/acts/tree.rb
...

acts_as_tree requires a foreign key called parent_id by default. Recall that we added parent_id to photos as part of the initial photos create_categories migration in Chapter 2. If you choose to use the default foreign key, Active Record will find it. Otherwise, you’d have to use the :foreign_key option. The methods and attributes added through acts_as_tree will use the foreign key to organize your tree, adding parent and children attributes to your class so each instance becomes a node of a tree. The root of the tree has a parent id of nil.

The acts_as_tree relationship is recursive, with an entity (Category) acting as both parent and children
Figure 4-6. The acts_as_tree relationship is recursive, with an entity (Category) acting as both parent and children

You’ve already got a Category class and a database table behind it with a parent_id. Let’s let Active Record manage the app/models/category.rb class:

class Category < ActiveRecord::Base
  has_and_belongs_to_many :photos
  acts_as_tree
end

If you’d like, you can order the children with :order modifier, but you don’t have to. The tree is ready to use as is. You just need a little test data. If you open up test/fixtures/categories.yml, you will notice this data is ready to be used:

category_1: 
  id: 1
  parent_id: nil
  name: All
category_2:
  id: 2
  parent_id: 1
  name: People
category_3:
  id: 3
  parent_id: 1
  name: Animals
category_4:
  id: 4
  parent_id: 1
  name: Places
category_5:
  id: 5
  parent_id: 1
  name: Things
category_6:
  id: 6
  parent_id: 2
  name: Friends
category_7:
  id: 7
  parent_id: 2
  name: Family

The parent_id defines the structure of the tree. Notice that the node named people has Friends and Family as children. Also, you can see that the root node, called All, has nil for a parent_id. Start or reload the console:

>> root = Category.find_by_parent_id(nil)
=> #<Category id: 1, parent_id: nil, name: "All", 
created_at: "2008-05-11 02:27:21", updated_at: "2008-05-11 02:27:21">
>> root.children.collect {|child| child.name}.join(', ')
=> "People, Animals, Places, Things"
>> root.children[0].children.collect {|child| child.name}.join(', ')
=> "Friends, Family"
>> Category.find_by_name('Family').parent.name
=> "People"

The children are dependent objects of the parents, so if you delete a parent, you’ll delete the children, too. Otherwise, what you’ve created is identical to a has_many relationship and a belongs_to relationship on category. Table 4-4 shows the methods and attributes added by the acts_as_tree relationship.

Table 4-4. Metaprogramming for acts_as_tree

Added feature

Description

Methods

All methods from has_many

A tree will have all of the methods of a has_many relationship, with children as the <associations> collection:

category.children.create

Attributes

Parent

category.parent

Children[]

An array of children:

category.children

What You Haven’t Seen

This implementation of categories is like a filesystem. Each directory has a set of files, just as each of our categories has a set of photos. Another macro, called acts_as_nested_set, would be useful if you wanted to find all of the folders in the All tree. Active Record is too big to cover in detail in such a short book, but we’ll point you to a few things that you should know:

Event callbacks

Active Record can call a method when an event happens, such as before a save, after creation, or when a model changes.

Reflection

Active Record will let you find attributes (as you’ve seen), and will also find all of the associations, or relationships, defined on a model.

Versioning

Active Record uses the column lock_version, if it exists, to manage concurrency using a technique called optimistic locking. With this technique, a database engine can store multiple versions of each piece of data and maintain database integrity if many applications need the same piece of data.

Count caching

Rather than using SQL to compute the number of certain types of objects, Active Record can cache the number of items in a collection for performance.

Polymorphic associations

You can design a model that has belongs_to relationships to more than one model. For example, an address can belong to a person and a shipment.

Enhancements

Active Record gets enhanced often. We recommend that you periodically check the documentation and watch the various Rails mailing lists if you’re going to be doing regular Rails development. Most importantly, Rails 2.1 supports dirty tracking and partial updates. You can use new features before they ship in the pre-release version of Rails called Edge Rails.

Looking Ahead

In the first four chapters, you learned how to build models, views, and controllers. In the next few chapters, we’ll continue to flesh out the Photo Share application. First, we’ll use scaffolding to rapidly build the user interface. Then, we’ll extend the resulting application through controllers and views. You’ll have a full working application a few hours from now.

With Safari, you learn the way you learn best. Get unlimited access to videos, live online training, learning paths, books, interactive tutorials, and more.

Start Free Trial

No credit card required