| Developer: Scripting
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
|