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 1)

by Gregory Brown
04/19/2007

Rails is advertised as a model-view-controller web framework. In this two-part article, we'll be focusing on the M in MVC. Specifically, we'll be talking about the object-relational mapping (ORM) software, ActiveRecord, that forms the core of any data-centric Rails application. ORM allows us to think in terms of objects and write in our programming language of choice (in this case, Ruby) while still getting the persistence and optimization benefits of SQL databases.

Though we could talk a lot about the theory behind data modeling and object relational mapping, perhaps the best way to learn Rails is by doing. The first part of this article will walk you through ORM fundamentals using ActiveRecord by example, and the second part will aim to build a small but functional application to show how ActiveRecord works within its setting and in concert with the other toolsets Rails provides.

Though you don't have to be a Ruby or Rails guru to understand this tutorial, having the most recent Ruby and Rails API documentation handy will probably make things easier to follow.

Tasty: The World's Best Social Bookmarking Half-Application

Social bookmarking is a simple set of concepts to model, which is why I've chosen it as the example for this article. We'll be modeling users, entries, and tags. Our implementation will be very simple, so you shouldn't consider it "production ready" by the end of this article. Also, note that in practice, you might consider using the very nice acts_as_taggable plugin for tagging support, but here we'll be rolling our own basic implementation. Besides, if Rails lets you build a blog in 15 minutes, I don't see why we can't build basic folksonomy in 5.

Let's jump right in and start building stuff. Since entries are probably the most straightforward to implement, we'll start there, just as soon as we get a project hooked up.

Setting Up Your Environment

First, let's create the project:

$ rails tasty -d sqlite3

I'll be using SQLite for simplicity here. Using SQLite with your Rails applications allows you to completely avoid configuring your database. Since it is file-based, you do not need to have a database service running to use your Rails app. I usually use the BeAlertWhenOnSqlite3 page from the Camping wiki to remind me how to get the adapter installed.

Entry: Our First Tasty Model

Now it's time to define the simplest model imaginable for entries for this little bookmarking app. We'll eventually need to hook in tagging and users, but right now, the most bare-bones set of attributes an Entry is likely to have is something like this:

So this would be enough to model a set of data such as:

url: http://redhanded.hobix.com
short_description: chunky bacon
long_description: The best darn chunky bacon resource on the net

That should be enough to get us going. We might want to do some timestamping, but Rails can actually help us out there, which you'll see in just a moment.

You'll notice I'm not overly worried about making changes to this model later. One place Rails really shines is through its migrations, which essentially provide a way to develop your database schema iteratively in pure Ruby. So if we need more fields later, or we need to implement different relationships with this data, we can do so down the line without much pain.

So let's write a simple schema, hook up the model, and load the stuff into the database.

$ ./script/generate model entry

You'll see this created a file called db/migrate/001_create_entries.rb . This will be where we define what attributes our Entry object will have (at least initially). If you're already familiar with SQL schema definitions, this shouldn't be too surprising to you. My definition follows:

Entry Migration (db/migrate/001_create_entries.rb)

class CreateEntries < ActiveRecord::Migration
 def self.up
   create_table :entries do |t|
     t.column :url, :string
     t.column :short_description, :string
     t.column :long_description, :text
     t.column :created_at, :datetime
     t.column :updated_at, :datetime
   end
 end

 def self.down
   drop_table :entries
 end
end


To load this into your database, just run 'rake db:migrate'. For now, that's all you'll need to do to begin populating this model with data!

It might be surprising that we've ignored implementing a unique identifier for our record. This is because Rails does this for us via an auto-incrementing integer attribute 'id', which is added by default in your migrations. You can change this behavior if necessary, but it's usually just one less thing to worry about when using Rails.

Basic CRUD Operations on Our Entry Model

CRUD stands for Create-Read-Update-Delete, and this is the core set of functionality that ActiveRecord provides.

Let's use the Rails console (a wrapper around Ruby's irb), to play with some of these basic features and add some data to our application:

$ script/console 
Loading development environment.

I'll be using Ruby's pp library to make these examples more readable; you can enable this and follow along at home like so:

>> require "pp"

Creation

We'll start by loading in our original example data, the hobix entry.

>> Entry.create( :url => "http://redhanded.hobix.com", 
?>                  :short_description => "chunky bacon",
?>                  :long_description => "The best darn chunky bacon resource on the net" )

Reading Records

If we ask for a listing of all the entries, you can see ours does indeed show up.

>> pp Entry.find(:all)
 [#<Entry:0x2b37d8ea1790
   @attributes=
    {"updated_at"=>"2007-04-06 17:41:07",
     "short_description"=>"chunky bacon",
     "url"=>"http://redhanded.hobix.com",
     "id"=>"1",
     "long_description"=>"The best darn chunky bacon resource on the net",
     "created_at"=>"2007-04-06 17:41:07"}>]

You'll see the data we asked it to create, as well as three automated fields, id, created_at, and updated_at. Right now the latter two are the same value, but you'll see this does the right thing in an upcoming example.

Since find(:all) actually returns an array of all of the records, we should try to make sure we can find the individual record as well. Here are a few ways to do that:

#by database id
>> Entry.find(1)

#by dynamic finder
>> Entry.find_by_url("http://redhanded.hobix.com")

The dynamic finder example may seem magical, but Rails will automatically enable these for all the attributes on your model. You can actually do much more complicated find operations, which we'll get into in the second part of this article.

Let's get back to the simple stuff. Reading attributes is quite easy as well.

>> entry = Entry.find_by_url("http://redhanded.hobix.com")
>> entry.short_description
=> "chunky bacon" 
>> entry.long_description 
=> "The best darn chunky bacon resource on the net"

Updating Records

This is where the ORM magic is starting to kick in. Updating is unsurprising as well:

>> entry.short_description = "Super Chunky Bacon" 
=> "Super Chunky Bacon" 
>> entry.save
=> true

Note that we do need to tell the object that we want to save the results back to the database.

There are other ways to do updates, which we'll show later.

I mentioned before that we'd be able to show that updated_at does indeed do the right thing. Dumping the object, you can see that this is the case:

>> pp entry
#<Entry:0x2b37d8e0b178
 @attributes=
  {"updated_at"=>Fri Apr 06 18:01:40 -0400 2007,
   "short_description"=>"Super Chunky Bacon",
   "url"=>"http://redhanded.hobix.com",
   "id"=>"1",
   "long_description"=>"The best darn chunky bacon resource on the net",
   "created_at"=>"2007-04-06 17:41:07"},
 @errors=
  #<ActiveRecord::Errors:0x2b37d8dcd5d0
   @base=#<Entry:0x2b37d8e0b178 ...>,
   @errors={}>>

Deleting Records

Finally, completing the CRUD circuit, let's confirm that we can kill off this entry and get Entry back to a blank slate:

>> entry.destroy
>> Entry.find(:all)   
=> []

Not Just CRUD: Validations and Simple Domain Logic

Though you can accomplish a lot with the four main data manipulation operations, you'll often need more than that for real applications. Validating data and adding domain-specific functionality are part of developing any real application.

We've worked with migrations already, but we haven't actually worked with our model definition yet. When we ran the script to create our model, it dropped a file in app/models called entry.rb

By default, it's pretty uninspiring:

class Entry < ActiveRecord::Base
end

Validation Basics

Let's start by adding a simple validation to our project. We'll need to move this later when we have multiple users, but for now, let's prevent entries from having duplicate URIs.

ActiveRecord makes this easy for us with a built-in validation:

class Entry < ActiveRecord::Base
  validates_uniqueness_of :url
end

Starting with an empty entries table, we'll create an entry and then try to create another, and then ensure that the validation captures the error.

>> Entry.find(:all)
=> []
>> wont_break = Entry.create(:url => "http://google.com", 
?>              :short_description => "google", 
?>              :long_description => "A Search Engine")
>> will_break = Entry.create(:url => "http://google.com", 
?>              :short_description=> "also google",
?>              :long_description => "A Search Engine...")
>> wont_break.valid?
=> true
>> will_break.valid?
=> false
>> will_break.errors["url"]
=> "has already been taken" 
>> Entry.find(:all).length
=> 1

Notice that only one record has been saved, and that you can get the errors for the url field by just inspecting the errors attribute.

Adding Custom Validations

There are a whole score of standard validations, but you can also create custom validations. The following code ensures that our short description does not include references to Rube Goldberg, for example:

  class Entry < ActiveRecord::Base
    validates_uniqueness_of :url

    protected

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

Here is an example that triggers the validation:

>> evil_goldberg = Entry.create(:url => "http://google.com",
?>   :short_description => "Rube Goldberg's favorite page",
?>   :long_description => "This is another failing create" )
>> evil_goldberg.valid?
=> false
>> evil_goldberg.errors.full_messages
=> ["Short description can't include references to Rube", "Url has already been taken"]

You can see that all errors are captured, not just the first one encountered. We'll add some more realistic validations once we start hooking everything up. The key thing to take away here is that you can add bits of code that can inspect data and look for potential errors before it is committed to your database. In practice, this feature set is essential.

Simple Domain Logic

You often will need to add custom domain logic in your models. Though this sounds fancy, it really just means Ruby methods that do something with your model.

For example, say we wanted a simplified representation of our created_at and updated_at fields, showing only the date, in the form of yyyy.mm.dd.

This is easy with Ruby's Time#strftime.

The following code is our model definition as it stands so far, with created_date and updated_date being our new methods.

class Entry < ActiveRecord::Base

  validates_uniqueness_of :url

  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

Now if you fire script/console back up, you can see how this works on your data.

>> Entry.find(1).created_date
=> "2007.04.10" 
>> Entry.find(1).created_at  
=> Tue Apr 10 09:07:02 -0400 2007
>> Entry.find(1).updated_date 
=> "2007.04.10" 
>> Entry.find(1).updated_at  
=> Tue Apr 10 09:07:02 -0400 2007

The possibilities here are really only limited to the concepts you are modeling and the functionality you'll need. The general idea is that the model should handle as much of its own data manipulation as possible, as it is the object that knows best what kinds of data it has to deal with.

This pretty much sums up the bare minimum of what you need to know to begin using ActiveRecord. For the visually oriented, your eyes might be glazing over looking at all this stuff on the command line. Let's work to get a simple view attached to your entries so you can see something, and then we'll build on this simple base in the next part of this article.

Get Ready to Fire Up Your Web Browser

You may have noticed that we've done all of our explorations so far through the Rails console, and we haven't even touched our browser once. As you become more experienced with Ruby and Rails development, this becomes a Ninja super power. However, I'm sure at least some of you are skeptical that we're actually building something here.

In the next part of this article, we'll actually tie this stuff together to build something you can use, adding some new functionality where needed.

For now, let's just get Rails to give us a simple list of entries.

We're going to create a controller/view combination now. What this will do is hook up our model to something that can actually be viewed in a browser. I won't go into detail about this here, but there are several tutorials that can help fill in background information.

Our controller name will be Entries, and we're just going to create a simple index page:

  $ script/generate controller entries

This will create a number of files and folders. The one we'll need to edit is app/controllers/entries_controller.rb.

For now, all we need it to do is grab the list of entries. The code looks like this:

class EntriesController < ApplicationController
  def index
    @entries = Entry.find(:all)
  end
end

Now, we'll need to throw together a little HTML to actually display our entries.

Under Rails conventions, the controller method name maps to the file name, so we'll want to put our view in app/views/entries/index.rhtml.

<html>
  <body>
    <h2> List of all entries </h2>
    <ul>
      <% @entries.each do |e| %>
        <li><%= e.short_description %> ( <%= e.url %> )</li>
      <% end %>
    </ul>
  </body>
</html>

The above code just goes through each entry in the database, and outputs the short description and the URL for that entry.

Go ahead and fire up the Rails server now.

$ script/server

Now open up your browser, and point it to http://localhost:3000/entries.

If all goes well, you should see something like this:


List of all entries


If you want, you can play around with adding, removing, and updating Entry objects using the previous CRUD examples in script/console.

Notice that you can just reload your browser to see the changes, and that there is no need to reboot script/server while in development mode.

That's All For Now

This is a lot of information to swallow, so it's time to take a break. You should have gained a basic understanding of how CRUD operations work in Rails, as well as how to perform simple validations and add domain logic to your models. You've probably also become pretty comfortable with script/console, which will be an indispensable tool in your Rails toolbelt. You even have something that loads up in your web browser that's able to talk to your database and display your data properly.

We don't quite have a social bookmarking application yet. We're still missing some of the pieces of that puzzle, which include relationships between tables, complex find operations, and some other fun ActiveRecord tricks. The next part of this article will fill in that information while we develop Tasty into something actually usable. Until then, you probably already have enough knowledge to do some more exploration of Rails, and I can't recommend enough the value of experimentation as a way to learn new systems.

Happy hacking!

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.