O'Reilly    
 Published on O'Reilly (http://oreilly.com/)
 See this if you're having trouble printing code examples


Understanding ActiveRecord: A Gentle Introduction to the Heart of Rails (Part 2)

by Gregory Brown
05/10/2007

It's now time to get back to building The World's Best Social Bookmarking half-app while learning about ActiveRecord, the Rails Object-Relational Mapper, along the way. If you have not read the first part of this article, you'll want to do that before continuing on here. It explains some core ActiveRecord concepts as well as some of the details about the app we're trying to build.

We still have a lot of ground to cover. The last time around, we got our Entry model up and running, but it was still a little simplistic. Besides, we are going to need a User model before we can officially be part of the WebTwoOh craze. That means we will need to understand how Rails handles relationships between models.

Modeling Relationships

The concept of the associations between tables in a database can get pretty deep if you let it. I took an entire course at my university dedicated to the relational algebra that describes it. Though that stuff is interesting, it's not really the level we want to be thinking at.

Instead, let's think in terms of our application: A user is going to have many entries, and those entries are going to have tags.

The code to describe this is going to be crazy easy, I promise. Still, for the visually minded, this diagram shows the bird's eye view of what we're trying to model.

figure

Hooking Up Our User Model

Relationships usually aren't much fun if they involve just one entity. Since the only thing we've got built so far is our Entry model, we still have to write code to introduce users and tags. For no particular reason, we'll start with the User model.

We're going to explore a few core ActiveRecord and Rails concepts along the way, so hopefully you won't mind taking the scenic route. If not, feel free to skim out the code you need and fly on through the next couple sections, meeting back up with us in the "Adding Entries to Users" section.

For those of you you decided to stay, let's generate our User model:

$ script/generate model user

As with the Entry model, this will dump a whole bunch of files we'll eventually need to get around to touching. Let's start with the table definition (db/migrate/002_create_users.rb)

A simple description of a User would be a model with a username, a display name, and an email address. We'll worry about hooking up entries in a bit, but we need to model those traits first. This is pretty much the same dance we did when creating the Entry model earlier.

Our migration will look like this:

  class CreateUsers < ActiveRecord::Migration
    def self.up                                                                        
      create_table :users do |t|
        t.column :username, :string                                                    
        t.column :display_name, :string
        t.column :email, :string
      end
    end 

    def self.down                                                                      
      drop_table :users                                                                
    end 
  end

For each new migration, we need to update our database to reflect the change:

  $ rake db:migrate

Introducing test/unit

Before we go to playing around with the model, let's make sure that it checks for unique user names and email addresses. We could just add validations to our model, then toy around in script/console to make sure that the proper errors are getting set up, but that gets old quickly, and it also doesn't do much for making sure that the behavior is maintained over time.

Instead, this time around we're going to write some unit tests. This is an automated way of making sure our expectations are being met, and once you get used to it, you'll never want to code without them. Since Rails apps dedicate a whole database to testing, we might as well make use of it.

Rails is designed to be test-driven, so it includes plenty of facilities to help you along. The first one we will need is fixtures, which allow you to set up some sample data to work against.

We will start with just one user, only filling in the essentials:

test/fixtures/users.yml
  one:
    id: 1
    username: sandal
    email: gregory.t.brown@gnospammail.com

We are going to want to check to see that a user needs to have a unique username and email address to be considered a valid record. To check this, we need three different cases.

test/unit/user_test.rb
require File.dirname(__FILE__) + '/../test_helper'

class UserTest < Test::Unit::TestCase

  fixtures :users
  def test_user_is_unique

    # -- user must have a unique username
    user = User.new( :username => "sandal",
                     :email => "greg7224@gnospammail.com" )

    assert(!user.valid?, "Should not save user unless username is unique")
    assert(user.errors.invalid?(:username), "Expected an error for duplicate username")

    # -- user must also have a unique email address
    user = User.new( :username => "shoe",
                     :email => "gregory.t.brown@gnospammail.com" )

    assert(!user.valid?, "Should not save user unless email is unique")
    assert(user.errors.invalid?(:email), "Expected an error for duplicate email")

    # -- If both username and email are unique, record should be considered valid.
    user = User.new( :username => "shoe",
                     :email => "greg@gnospammail.com" )

    assert(user.valid?, "Expected to save record but did not")
  end
end

That might look a little imposing at first, but if you look at it, we're really just codifying our expectations. We haven't added the validations yet, so we should expect these tests to fail, and they do, giving us a helpful message (truncated for clarity):

  $ rake test

    1) Failure:
    test_user_is_unique(UserTest) [./test/unit/user_test.rb:10]: 
    Should not save user unless username is unique.
    <false> is not true.

Let's add the username validation to our model(app/models/user.rb) and run the tests again.

  class User < ActiveRecord::Base  
     validates_uniqueness_of :username
  end

Sure enough, adding the declaration above makes some progress, we get a new failure.

  1) Failure:
  test_user_is_unique(UserTest) [./test/unit/user_test.rb:21]:
  Should not save user unless email is unique.
  <false> is not true.

By editing the validation to include the email address, all of our tests pass:

  
  class User < ActiveRecord::Base  
     validates_uniqueness_of :username, :email
  end

This is a bit of an oversimplified example, but hopefully you're already feeling the confidence boost of having tests covering your expectations. Most good Rails programmers practice Test Driven Development, so it is a good habit to get into. Also, this is only scratching the surface of the kind of testing you can do, but since that's an entire topic on it's own, this will have to suffice for now.

Adding Entries to Users

Now that we have a User model, we can hook up Entry to it. The type of association we're dealing with for this particular relationship is one-to-many. Any given user will have many entries, but any given entry will belong to exactly one user. The relevant keywords for this in Rails are has_many and belongs_to.

I promised the code would be crazy easy, and it is. Here are the updated definitions for Entry and User after adding the relationship, note that it's a total change of two lines of code:

class User < ActiveRecord::Base
  validates_uniqueness_of :username,:email

  # -- tell User it has a collection of Entry objects associated with it
  has_many :entries
end

class Entry < ActiveRecord::Base

  validates_uniqueness_of :url

  # -- Tell Entry that it has a unique User associated with it.
  belongs_to :user

  def created_date
    created_at.strftime("%Y.%m.%d")
  end

  def updated_date
    updated_at.strftime("%Y.%m.%d")
  end

  protected

  def validate
    if short_description =~ /rube goldberg/i
      errors.add("short_description", "can't include references to Rube")
    end
  end

end

There is one small change we need to make to our database schema before this will work. Most of the grunt work is handled by Rails conventions, but our Entry records need to have a place to store the user's database ID for this all to work. We can add a column for this with a simple migration.

$ script/generate migration add_user_id_to_entry
class AddUserIdToEntry < ActiveRecord::Migration
  def self.up
    add_column(:entries,:user_id,:integer)
  end

  def self.down
    remove_column(:entries,:user_id)
  end
end

When you run rake db:migrate it should add the user_id column, and we've now successfully hooked up User to Entry. Here is a quick script/console session to show our progress. I've narrowed the output down just to the interesting bits.

>> user = User.create(:username => "sandal", :email => "gregory@test.com")
>> user.entries
=> []
>> user.entries.create(:url => "google.com", :short_description => "Search Engine")
>> user.entries.find_by_url("google.com").short_description
=> "Search Engine" 
>> user.entries.length                                     
=> 1

Tying It All Together with Tags

W're already two-thirds of the way to our half-app, without too much work. However, the trickiest part of this tutorial will certainly be hooking up the Tag model in a way that really gets us where we need to be.

If we just wanted to have each entry have its own tags, that'd be pretty straightforward, mostly just repeating what we just did with User and Entry. The trouble is that we really want to also be able to go in the other direction as well, looking up entries by tag name.

This diagram illustrates the other half of the relationship that was left out of the first diagram:

figure

This is a many-to-many relationships, where each tag may potentially reference many entries, and each entry may have many tags. Rails has a facility for this called has_and_belongs_to_many, but for a number of reasons, the cool kids often use has_many :through. It tends to be the right choice whenever you might need to change things down the line, so that is the method we'll use.

Let's start with the Tag model. All we really need is a name attribute, and we probably want it to be unique.

You're probably familiar with how to script up the boilerplate by now, so I'll just list my migration and model definitions here, and leave it to you to do the rest:

db/migrate/004_create_tags.rb
class CreateTags < ActiveRecord::Migration
  def self.up
    create_table :tags do |t|
      t.column :name, :string
    end
  end

  def self.down
    drop_table :tags
  end
end
app/models/tag.rb
class Tag < ActiveRecord::Base
  validates_uniqueness_of :name
  validates_presence_of :name
end

Building Our Join Table

I'm going to steal the table name from the acts_as_taggable Rails plugin for this concept: Tagging. The Tagging model will be what we route our has_many :through calls, and will form the final model in our app.

It's just a regular model, so you generate it the same way as all the others. The table definition looks like this:

db/migrate/005_create_taggings.rb
class CreateTaggings < ActiveRecord::Migration
  def self.up
    create_table :taggings do |t|
      t.column :entry_id, :integer
      t.column :tag_id, :integer
    end
  end

  def self.down
    drop_table :taggings
  end
end

If you haven't run rake db:migrate in a while, now is the time to do that. We'll now add to our models the definitions necessary to set up our many to many relationship. We need to tell Rails a little more about the relationship this time around, and this is simply due to the fact that we don't need to modify the Entry or Tag table definition to set up the relationship.

At any rate, the general idea is that an Entry has many tags, and independently, a Tag is associated with many entries, so each model has many "taggings" associated with it. A single Tagging defines the link between an Entry and a Tag.

Here is how we translate that idea to Rails, in each of our models:

app/models/entry.rb
class Entry < ActiveRecord::Base
  # rest of definitions omitted
  has_many :taggings
  has_many :tags, :through => :taggings
app/models/tag.rb
class Tag < ActiveRecord::Base
  #rest of definitions omitted
  has_many :taggings
  has_many :entries, :through => :taggings
end
app/models/taggings.rb
class Tagging < ActiveRecord::Base
  belongs_to :tag
  belongs_to :entry
end

Before we go further, we should add some methods to our Entry and Tag that will simplify working with tags.

class Entry < ActiveRecord::Base

  # rest of definitions omitted

  # Adds a tag with the given name, if it's not already present
  def tag_as(tagname)
    unless tagged_as?(tagname)
      tags << Tag.find_or_create_by_name(tagname)
    end 
  end

  # True if tags include a Tag with the given name, False otherwise
  def tagged_as?(tagname)
    tag_names.include?(tagname)
  end

  # returns a list of tag names
  def tag_names
    tags.map(&:name)
  end

end

This brings us very close to where we need to be: We can actually model all the data from the diagram at the beginning of this article. Let's start from a blank slate by wiping all our data. Run the following commands, and then fire up the console:

rake db:migrate VERSION=0
rake db:migrate

This explains what all those self.down methods are for in your migrations: Rails lets you rewind your schema changes as needed, which is exactly what we just did.

Slowly Reaching the Fun Part

Here is my console session, which created something similar to what the first diagram showed:

>> joe = User.create(:username => "joe", :email => "joe@test.com", 
                     :display_name => "Joe User")
>> al = User.create(:username => "al", 
                    :email => "al@test.com", :display_name => "Al Hacker")

>> joe.entries.create(:url => "redhanded.hobix.com", :short_description => "_why's blog")
>> joe.entries.create(:url => "rubyreports.org",:short_description => "Ruby Reporting lib")
>> rh = joe.entries.find_by_url("redhanded.hobix.com")           
>> rh.tag_as "chunky_bacon" 
>> rh.tag_as "_why"        

>> rp = joe.entries.find_by_url("rubyreports.org")
>> rp.tag_as("awesome")
>> rp.tag_as("reporting")

>> al.entries.create(:url => "google.com", :short_description => "Search Engine")
>> al.entries.create(:url => "notrubyreports.org", 
?>                   :short_description => "Lame Reporting")

>> gg = al.entries.find_by_url("google.com")
>> gg.tag_as("search")
>> gg.tag_as("borg")  

>> nrr = al.entries.find_by_url("notrubyreports.org") 
>> nrr.tag_as("boring")
>> nrr.tag_as("reporting")

And here is some evidence that it's all working:

>> Tag.find_by_name("reporting").entries.map { |e| e.url }
=> ["rubyreports.org", "notrubyreports.org"]
>> Tag.find_by_name("reporting").entries.map { |e| e.user.display_name }
=> ["Joe User", "Al Hacker"]
>> al.entries.find_by_url("google.com").tag_names 
=> ["search", "borg"]

Making Things Prettier

Tag.find_by_name(tagname).entries looks like something we'd probably be typing a lot, and Tag.entries_by_name might be a little easier on the eyes. We can do this by creating a class method on Tag, which looks like this:

class Tag < ActiveRecord::Base

  # rest of stuff omitted

  def self.entries_by_name(tagname)
    my_tag = find_by_name(tagname)
    my_tag.entries.uniq if my_tag
  end

end

The end result is something like this:

>> Tag.entries_by_name("reporting").map { |r| r.url } 
=> ["rubyreports.org", "notrubyreports.org"]

Back to the Browser

Here's an updated view for entries to show you the new features visually:

app/views/entries/index.rhtml
<html>                                                                               
  <body>                                                                             
    <h2> List of all entries </h2>                                                   
    <ul>                                                                             
      <% @entries.each do |e| %>                                                     
        <li><%= e.short_description %> ( <%= e.url %> )<br>                          
            Tags: <em><%= e.tag_names.join(" , " ) %></em><br>                       
            Posted By: <%= e.user.display_name %><br>                                
        </li>                                                                        
        <br>                                                                         
      <% end %>                                                                      
    </ul>                                                                            
  </body>                                                                            
</html>

If all goes well, when you browse to http://localhost:3000/entries you should see this:

figure

It'd be nice to see a view of the tags, too, since we went through the added trouble to create them. Generate the controller like this:

$ script/generate controller tags

My controller and view for tags are shown below:

app/controllers/tags_controller.rb
class TagsController < ApplicationController                                         
  def index                                                                          
    tag_names = Tag.find(:all).map(&:name)                                           
    @urls_for_tags = {}                                                              
    tag_names.each do |name|  
      @urls_for_tags[name] = Tag.entries_by_name(name).map { |r| r.url }             
    end     
  end       
end
app/views/tags/index.rhtml

<h2> URLs for All Tags </h2>
   <% @urls_for_tags.each do |tag,url_list| %>
   <h3><%= tag %></h3>
   <ul>
     <% url_list.each do |url| %>
       <li><%= url %></li>
     <% end %>
   </ul>
   <% end %>

Assuming all goes well, http://localhost:3000/tags should look like this:

figure

Not exactly "beautiful," but definitely functional. With a little form building love and some linking, you pretty much have an app.

The Light at the End of the Tunnel

ActiveRecord is a big system with a whole lot of features. The good news is, if you've been able to follow the code in this article, you've just covered a huge chunk of the most common database tasks you'll need to do in your Rails applications. The tricky stuff comes easy once you get past the first hurdle of actually getting something running.

Because I couldn't delve too deep into any given topic, there are definitely some holes you'll want to fill in if you want to master ActiveRecord as a whole. In terms of relationships, I left out has_one because it should be easy to understand now that you know how to do the more complicated associations shown. I've also left out the important topic of polymorphic associations, which can be very powerful for certain types of modeling. You'll also want to dig a whole lot deeper than I did in this article in terms of testing your applications, which is something that you'll always be thankful for in the long run.

Beyond that, there is a lot more interesting stuff that could be said in advanced ActiveRecord discussions. But most of the stuff you need to know, you'll be able to find when you need it. For now, you probably have the skills necessary to populate the world with yet another socially-networked, uber-cool, ultra-sexy whathaveyou.

The cool thing about ActiveRecord is that it is useful both in general Ruby applications, as well as in the quirky-yet-lovable web framework Camping. So if Rails isn't the only thing on your mind, having a solid base in AR may still be useful to you.

The full source for the code from this tutorial is available under the License of Ruby, so if you'd like to play with it, feel free.

Gregory Brown is a New Haven, CT based Rubyist who spends most of his time on free software projects in Ruby. He is the original author of Ruby Reports.


Return to Ruby.

Copyright © 2009 O'Reilly Media, Inc.