Developer: Scripting
  DOWNLOAD
Oracle Database XE
Ruby on Rails
  TAGS
ruby, scripting, rubyonrails, All

Ruby on Rails Active Record 101


by Bruce Tate

An insider's look at Active Record, the persistence engine behind Ruby on Rails

Published March 2007

As a frequent kayaker, I'm often encountered rapids I've not seen before. But I've been boating for a long time, so only something radically different will capture my imagination.

In March 1998, I found myself looking down at an odd stretch of river. All of the current piled high on a set of rocks, and formed a natural perfectly shaped staircase, with each step angled toward the steep cliffs of the riverbed. The symmetry and shape of the rapid formation blew me away. We stood on the bank looking down at the formation in awe and fear for hours.

After nearly 10 years of writing Java, I encountered Ruby on Rails, and the persistence engine called Active Record. The experience was eerily similar to that weekend in 1998. In Java, I often encountered advanced object-relational mapping frameworks such as Oracle TopLink, JBoss Hibernate, Enterprise JavaBeans, and Java Data Objects. I also encountered primitive wrapping frameworks that could generate code, providing a thin object wrapper around a relational database table. None of this prepared me for the beautiful, simple approach to persistence that Active Record takes. It was something entirely new, and remarkably different in approach and simplicity.

In this article, I'll work with you to put Active Record through its paces. You'll start by building an Oracle database schema through Rails migrations. You'll then create some model objects, and put those objects to work within the Rails console. You'll then add two different kinds of relationships to your model: belongs_to and has_many. Finally, you'll use the Rails test framework to populate the model with test data. When you're done, you may not agree with all of the compromises that the Rails inventors made, but you will be hard-pressed to argue against the beauty in Active Record. Like my experience on the river in 1998, Active Record often makes me step back to take notice.

Installing Rails

Before you can start using Active Record to build applications, you're going to need to install three things: the Ruby programming language, the Rails framework, and a database with a Ruby driver. You can get the Ruby and Rails downloads at http://rubyonrails.com/down. If you're using Windows, use the one-click installer. If you are using another platform, get the appropriate downloads for Ruby version 1.8.4 and gems, the package installer for Ruby, and follow the instructions to install Rails using Gems.

You'll also need to install and configure your Oracle drivers. You can download them from http://rubyforge.org/projects/ruby-oci8. You can then follow the directions you find there to install your Oracle driver, which works with OCI versions 8, 9, and 10. I'm going to assume you've installed a working Rails installation that can talk to your chosen database. For my examples, I'll be using Rails version 1.1.6. You'll want to be using at least this release, because it fixes a major security hole from earlier versions.

Setting Up: Creating a Rails Project

To get started, you're going to create a Rails project. All Rails projects have the same basic directory structure, and we'll go through the highlights after you do the install. Go to the directory where you'd like to install your project, and change to the team directory by typing:

> rails team
...lots of output...
> cd team
You'll see the output in Listing 1.

Listing 1

create  app/controllers
create  app/helpers
create  app/models
create  app/views/layouts
create  config/environments
create  components
create  db
create  doc
create  lib
create  lib/tasks
create  log
create  public/images
create  public/javascripts
create  public/stylesheets
create  script/performance
create  script/process
create  test/fixtures
create  test/functional
create  test/integration
create  test/mocks/development
create  test/mocks/test
create  test/unit
create  vendor
create  vendor/plugins
create  tmp/sessions
create  tmp/sockets
create  tmp/cache
create  Rakefile
create  README
create  app/controllers/application.rb
create  app/helpers/application_helper.rb
create  test/test_helper.rb
create  config/database.yml
create  config/routes.rb
create  public/.htaccess
create  config/boot.rb
create  config/environment.rb
create  config/environments/production.rb
create  config/environments/development.rb
create  config/environments/test.rb
create  script/about
create  script/breakpointer
create  script/console
create  script/destroy
create  script/generate
create  script/performance/benchmarker
create  script/performance/profiler
create  script/process/reaper
create  script/process/spawner
create  script/runner
create  script/server
create  script/plugin
create  public/dispatch.rb
create  public/dispatch.cgi
create  public/dispatch.fcgi
create  public/404.html
create  public/500.html
create  public/index.html
create  public/favicon.ico
create  public/robots.txt
create  public/images/rails.png
create  public/javascripts/prototype.js
create  public/javascripts/effects.js
create  public/javascripts/dragdrop.js
create  public/javascripts/controls.js
create  public/javascripts/application.js
create  doc/README_FOR_APP
create  log/server.log
create  log/production.log
create  log/development.log
create  log/test.log

Explore this directory structure. Though you've done nothing yet, you can see that Rails has already done some work for you by organizing your development structure and giving you the tools you need to build your application. You'll see many directories, including those below:

  • app: This directory contains subdirectories for your models, views, and controllers, among other things. In this article, since you're going to build a database-backed model, most of your code will go into the app/models directory.
  • config: Your database configuration, and the configuration of your environments, will go here. As you work with Rails, you'll notice that you don't need that much configuration at all. Rails uses conventions, where possible, instead of configuration. If you want to use an Oracle database instead of the default mysql database, you'll want to edit the file database.yml. Also, if you want to change from the default root/no-password installation, you'll need to change database.yml. That's the entire configuration you'll need to write!
  • db: All of your database scripts will go here. One special kind of script, called a migration, will be in the db/migrations directory. All of the code that creates your schema will go in this directory. You don't have any migrations yet, so the migrations subdirectory does not yet exist.
  • log: The Log directory has log files containing information and error messages. If you make any mistakes over the coarse of the article, you can always check the log files for errors.
  • public: This is the root directory for your web server. All static content, such as images and style sheets, go in this directory.
  • script: come with a set of scripts that we'll use to generate code, start a development web server instance, debug our applications, and work with objects in an interpreter.
  • test: The test subdirectory has unit tests, functional tests, integration tests, and files with test data called fixtures. You'll use fixtures in this article, and unit tests for your database-backed models would go here.

As you work with Rails projects, you'll find that every Rails project shares this structure. A Rails developer can easily go from project to project and quickly become productive. Some common directories will become quickly familiar. Most of your code goes in app, your images and other static web content go into public, and your database configuration will go into config/database.yml. Go ahead and open that file now.

You can enter the configuration that is appropriate for your database. The default file works from MySQL. For Oracle, a typical database.yml file will look like this:

	  
development:
  adapter: oci
  username: admin
  password: password
  host: localhost/team_development

test:
  adapter: oci
  username: admin
  password: password
  host: localhost/team_test	  

Make sure you configure different databases for development and test, because running tests will delete all of the data in your tables. You should also go ahead and create databases with your chosen database engine. I'll stick with the default database names, and create databases called team_development and team_test. Create your user with the appropriate password, and grant it dba access for now. (Your production configuration will likely vary.)

Congratulations. You've done all of the setup and configuration that you need for active record development using Rails. You have Ruby and Rails installations, databases for test and development, and a database configuration.

Building your Schema: Migrations

In this article, you're going to focus purely on Active Record. When you're done, you'll have a working database-backed model and an understanding of some critical Active Record elements, including:

  • Rails migrations, which build your database schema, and help you manage changes in data between releases. Migrations are short programs that let you incrementally create your database schema, stepping forward or back based on your development or production needs.
  • Rails model objects, which use Active Record to place Ruby wrappers around each table in your model.
  • Fixtures, containing test data. These fixtures will give you some sample data for development, and also allow you to build meaningful, repeatable tests.
  • Tests. Dynamic languages like Ruby don't give you all of the safety of a compiler, which catches certain kinds of errors. Instead, you'll lean heavily on automated unit tests to catch simple type errors as well as errors in logic.
The first step in building your database objects is to use the convenient Rails generator to build files containing empty shells for each of the above items. You'll use the Rails generator for each model you wish to create. I'm going to assume that you know about primary keys and foreign keys, but if you don't, just use the examples exactly as I show them.

To find out exactly what files you can generate, simply type ruby script/generate on Windows or script/generate on *nix or Mac OS X. For the rest of this article, if you're running Windows, remember to precede any script command with Ruby, and you'll be fine. To find out more about using the model generator, type script/generate model without any options. Output is shown in Listings 2 and 3.

Listing 2 Output for script/generate

Usage: script/generate generator [options] [args]

General Options:
    -p, --pretend         Run but do not make any changes.
    -f, --force           Overwrite files that already exist.
    -s, --skip            Skip files that already exist.
    -q, --quiet           Suppress normal output.
    -t, --backtrace       Debugging: show backtrace on errors.
    -h, --help            Show this help message.
    -c, --svn             Modify files with subversion. (Note: svn must be in path)

... more output deleted ...

Listing 3 Output for script/generate model

Usage: script/generate model ModelName [options]

Options:
        --skip-migration  Don't generate a migration file for this model

General Options:
    -p, --pretend         Run but do not make any changes.
    -f, --force           Overwrite files that already exist.
    -s, --skip            Skip files that already exist.
    -q, --quiet           Suppress normal output.
    -t, --backtrace       Debugging: show backtrace on errors.
    -h, --help            Show this help message.
    -c, --svn             Modify files with subversion. (Note: svn must be in path)

Description:
    The model generator creates stubs for a new model.

...more output deleted...

To get started, type script/generate model Team, and then script/generate model Player. You'll see a list of files that each command creates, as in Listings 4 and 5.

Listing 4

script/generate model team 
      exists  app/models/
      exists  test/unit/
      exists  test/fixtures/
      create  app/models/team.rb
      create  test/unit/team_test.rb
      create  test/fixtures/teams.yml
      create  db/migrate
      create  db/migrate/001_create_teams.rb

Listing 5

script/generate model player
      exists  app/models/
      exists  test/unit/
      exists  test/fixtures/
      create  app/models/player.rb
      create  test/unit/player_test.rb
      create  test/fixtures/players.yml
      exists  db/migrate
      create  db/migrate/002_create_players.rb 

You'll notice that two of the files that were created are migrations, one each for team (001_create_teams.rb) and player (002_create_players.rb). You should immediately notice a few conventions. First, you can see that each migration is numbered. (You'll see why shortly.) Second, you'll notice that by convention, the name of a database table is the English plural of the model object. Rails relies heavily on conventions, and if you follow the standard Rails conventions, you'll find that you wind up writing much less code. These are some important conventions:

  • Model names are capitalized (as are all Ruby class names), and specified in CamelCase (a convention capitalizing the letter of each new word.)
  • Method names, database tables, and symbols use underscores to separate each separate word, like_this.
  • M
  • Model names use the English singular form, such as player or team.
  • Table names use the English plural form, such as players or teams.
  • The primary key for each table is an auto-numbered sequence of integers named id.
  • Foreign keys, used for one-to-one and one-to-many relationships, contain the singular form of the foreign table, followed by _id, such as team_id. They use underscores to separate words rather than camel case.
  • Several column names have special meanings, including type (the class name for tables using inheritance), position (the numerical position for list items), parent_id (the name of the parent row for trees), and a few others. In each case, you can override the defaults.
  • Join tables, used in many-to-many relationships, use the singular form of each table, separated by underscores, in alphabetical order, such as team_city.

These are the most important conventions that you'll need to keep in mind as you build your model. For now, go ahead and build the migrations for Team and Player.

First, think about design. One team has many players, so each player will have a foreign key. By convention, the primary key is called id, and the foreign key is called team_id.

Go ahead and edit your migrations to look like Listings 6 and 7. A Rails migration is a simple class with two methods: up and down. To apply a migration, you can run the up method for each migration, in order. To move back down, you can run the down methods in inverse order. In this way, you can make changes to your database schema, but also to your data if needed. As you would expect, your up methods create tables, and the down methods destroy them. A future migration may alter a table, add indexes, add, remove, or rename columns, or even delete the table altogether.

Listing 6

class CreateTeams < ActiveRecord::Migration
  def self.up
    create_table :teams do |t|
      t.column :name, :string
      t.column :city, :string
      t.column :sport, :string
    end
  end

  def self.down
    drop_table :teams
  end
end

Listing 7

def self.up
    create_table :players do |t|
      t.column :name, :string
      t.column :position, :string
      t.column :number, :integer
      t.column :team_id, :integer
    end
  end

  def self.down
    drop_table :players
  end
end

The Ruby tool rake applies the migrations. Type the command rake migrate, and you'll see the results in Listing 8. If you get a message that says "unknown database team development," Rails is not finding your database. You either need to create the database, or fix your configuration in database.yml. If you make a mistake with your code and get stuck, fix the mistake in your code, drop the database, and start over. (Later, I'll show you how to recover from problems.)

Listing 8

bruce-tates-computer:~/rails/team batate$ rake migrate
(in /Users/batate/rails/team)
== CreateTeams: migrating =====================================================
-- create_table(:teams)
   -> 0.0027s
== CreateTeams: migrated (0.0029s) ============================================

== CreatePlayers: migrating ===================================================
-- create_table(:players)
   -> 0.0024s
== CreatePlayers: migrated (0.0025s) ==========================================

You could move directly to a specific migration, you can type rake migrate VERSION=1 or some other number. Rails will apply the appropriate up or down methods in the appropriate order. Rails maintains the current migration number in the database using a table called schema_info, which has a single column called version and a single row. Sometimes, you can have errors in your migration that leave a given migration in an inconsistent state. For example, your migration might try to create one table and drop another. If one operation succeeds and the other fails, you need to manually intervene, usually by making changes that take you back to the previous migration level. If you need to, you can update the schema_info table directly.

Another problem with migrations is that two members of a development team might generate different migrations at the same time, and check them in. You'd then have two migrations with the same number, and Rails would not know which to apply first. In such a case, you typically have to manually back out the changes, and manually renumber your migrations. The migrations feature is not perfect, but it does begin to solve the problem of providing an automated scheme to manage differences in your database schema as your development progresses. Such a process is sorely lacking in most development environments.

Now that you have a working schema, it's time to populate it with some data. You could use a SQL script for this purpose, but a better solution is to use a feature called test fixtures.

Edit the files test/fixtures/teams.yml and test/fixtures/players.yml to resemble Listings 9 and 10. These files contain data that your test cases will eventually use.

Listing 9

# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html
titans:
  id: 1
  name: Titans
  city: Nashville
  sport: football
raiders:
  id: 2
  name: Raiders
  city: Oakland
  sport: football

Listing 10

# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html
young:
  id: 1
  name: Vince Young
  number: 10
  position: quarterback
  team_id: 1
huff:
  id: 2
  name: Michael Huff
  number: 7
  position: free safety
  team_id: 2	  

These files are in yml, a data definition language much simpler than XML. Keep in mind that white space is significant, so don't pad rows with extra spaces, maintain an indentation of two spaces, or use spaces instead of tabs. When you're done, you can type rake load fixtures to populate your database with this test data. Later, your unit tests will use a fresh copy of this data to make sure each unit test starts with the same set of data, so your tests remain repeatable.

Listing 11

class Player < ActiveRecord::Base
end  

Believe it or not, you already have two working models, which do almost everything you need it to do. You can already use the model in the Rails console, a marvelous tool that lets you play with your persistent model in a Ruby interpreter. Start it up by typing script/console. You'll see a command line with a prompt. Using this command line, you can manipulate your Active Record objects directly. If you've never used Ruby before, you should know that each Ruby expression returns a value, and typing an expression in the console will show you the returned value in the following line.

Though the generated models in listing 11 were sparse, they pack a serious punch. Type Team.new; Ruby will return an empty Team object with a value something like #<Team:0x28f8618 @attributes={"city"=>nil, "name"=>nil, "sport"=>nil}, @new_record=true>. Notice the attributes. Somehow, Active Record inferred all of the attribute names, including city, name, and sport! Active Record does this magic through convention. Ruby knew the name of the class through reflection (a language facility allowing an object to determine its attributes and methods.) Through this name and Rails conventions, Active Record determined the name of the table: teams. Next, the nimble framework went to the database to get the names and types of the attributes. Finally, Active Record used Ruby's metaprogramming capabilities to add an attribute to your class for each column in the database.

Active Record has some interesting behaviors that you may not expect, if you've been using static languages such as Java and C#. One of them is dynamic finders. You can load an object by calling the find method on the class object, and passing in an id. For example, you can find the row for Vince Young with young = Player.find 1. You can also find by one or more attributes. For example, you can type:

Player.find_by_number_and_position 10, "quarterback"	 

Active Record dutifully returns Vince Young. Active Record accomplishes this magic through overriding method_missing, the method Ruby invokes when a method is not found. Active Record then parses the method name for find_by and the list of attributes, separated by _and_.

Active Record can also create, delete, and update records. To delete a record, you can use the method destroy. To insert a new record, you can use the method save. To update a record, use the method update name value or update_attributes hash , passing in the name/value pairs of the parameters you want to update, in the form of strings or a hash map. Listing 12 shows a console record of each of these commands in action, within the Rails console.

Listing 12

>> player = Player.new
=> #<Player:0x28ef720 @attributes={"team_id"=>nil, "name"=>nil, "number"=>nil, "position"=>nil}, 
@new_record=true>
>> player.name = "Drew Kelson"
=> "Drew Kelson"
>> player.number = 4
=> 4
>> player.position = "safety"
=> "safety"
>> player.save
=> true


>> player.update_attribute :position, "linebacker"
=> true
>> player.reload
=> #<Player:0x28ef720 @attributes={"team_id"=>nil, "name"=>"Drew Kelson", 
"number"=>"4", "id"=>"3", "position"=>"linebacker"}, 
@new_record=false, @errors=#<ActiveRecord::Errors:0x28e1d50 @base=#<Player:0x28ef720 ...>, 
@errors={}>>
>> player.position
=> "linebacker"

>> player.destroy         
=> #<Player:0x28ef720 @attributes={"team_id"=>nil, "name"=>"Drew Kelson", 
"number"=>"4", "id"=>"3", "position"=>"linebacker"}, 
@new_record=false, @errors=#<ActiveRecord::Errors:0x28e1d50 @base=#<Player:0x28ef720 ...>,
@errors={}>>
>> Player.find_by_position "linebacker"
=> nil


>> player = Player.new=> #<Player:0x28815e0 @attributes={"team_id"=>nil, "name"=>nil, "number"=>nil, 
"position"=>nil}, @new_record=true>
>> player.update_attributes :name => "Drew Kelson", :position => "Linebacker", :number => 4
=> true
>> player.reload 
=> #<Player:0x28815e0 @attributes={"team_id"=>nil, "name"=>"Drew Kelson", 
"number"=>"4", "id"=>"5", "position"=>"Linebacker"}, 
@new_record=false, @errors=#<ActiveRecord::Errors:0x286e724 @base=#<Player:0x28815e0 ...>, 
@errors={}>>
>> player.name
=> "Drew Kelson"

Extending the Application with Relationships

So far, the two models work well, but nothing ties them together, save the primary key. Adding relationships to them will be nearly trivial.

Review the database structure. You've defined a many-to-one relationship between players and teams, with a team_id in the players table pointing to an id in team. Right now, the relationships are in the database, but not the object model. You need to add the relationships by hand, because Rails cannot always guess what the relationship should be. (For example, the database structure for has_one is identical to the structure for has_many.) So, edit app/models/team.rb and app/models/player.rb to resemble Listing 13.

Listing 13

class Team < ActiveRecord::Base
  has_many :players
end

class Player < ActiveRecord::Base
  belongs_to :team
end

Quit the console and reload it to load your changes in the model. Now, you can type vy = Player.find 1, followed by vy.team.name to get the result Titans. You can also go the other direction, but a Team has more than one player, so you'd type Team.find(1).players. You can see the results in Listing 14.

Listing 14

>> exit
bruce-tates-computer:~/rails/team batate$ script/console 
Loading development environment.
>> vy = Player.find 1
=> #<Player:0x2926f90 @attributes={"team_id"=>"1", "name"=>"Vince Young", "number"=>"10", 
"id"=>"1", "position"=>"quarterback"}>
>> vy.team.name
=> "Titans"

To drill a little deeper, use the console to find the type represented by the Active Record class. Type team = Team.find 2. Next, type team.players.class. You'll find, as you might expect, that players is an array. Adding a player or removing a player is easy. To add, you'd type team.players << new_player. To delete one, you'd type team.player[0].remove.

This model does what you need it to, so it's time to write some tests.

Powerful, But Not For Every Application

Here you've only scratched the surface of Active Record. It's a powerful framework—much more powerful than other frameworks that implement the Active Record design pattern. It leverages the strengths of the Ruby framework—a simple syntax, metaprogramming, and the ability to open and manipulate classes at run time—to form a marvelously effective and productive environment for green-field development.

But Active Record is not for every application. It does not handle legacy schemas nearly as well. For those, you need a mapping framework, not one that merely wraps tables with simple classes.

Active Record also is prone to performance problems for those who don't have the discipline or skill to tune it properly. Active Record, and the Ruby on Rails framework, represent what founder David Heinemeier-Hannsen calls opinionated software. Rails, and Active Record by extension, represents hundreds of compromises, with most of them favoring a beautiful, productive programmer experience.


Bruce Tate [http://blog.rapidred.com] is a father, kayaker, author and independent consultant in Austin, Texas. He worked for 13 years at IBM, in roles ranging from a database systems programmer to Java consultant. In the past five years, he established his consulting practice, called RapidRed, with emphasis on lightweight development in Ruby and persistence strategies. He is the author of nine books, including the Jolt Award-winning Better, Faster, Lighter Java (O'Reilly, 2004), From Java to Ruby (Pragmatic Bookshelf, 2006) and Ruby on Rails, Up and Running (O'Reilly, 2006).

Send us your comments
E-mail this page
Printer View Printer View
Oracle Is The Information Company About Oracle | Oracle RSS Feeds | Careers | Contact Us | Site Maps | Legal Notices | Terms of Use | Privacy