Rails - Database Access

One of the driving forces of Rails is to make things easier for developers. It does this partly by taking decisions out of developers hands. It's an 'opinionated' framework, and one of the opinions it has is on the pattern to use for database access. Its choice in this case is the 'active record' pattern.

Rails has an ActiveRecord module and the model classes all derive from

ActiveRecord::Base
for example
BlogEntry < ActiveRecord::Base

it is this module that provides the active record support for the framework.

Like much of the rest of Rails, ActiveRecord follows naming conventions. Here for example the BlogEntry class represents a row in the blog_entries table. How does this happen?

Looking back to the Learning Rails - Part 2 post you will see that this script was run

script/generate scaffold blog_entry ...

This script created two files, the file with the model class BlogEntry definition and a "migration". The migrations are "scripts" that help create and mange the database definitions, essentially they are DDL for Rails.

Migrations are used to both set up and tear down databases. The files contain class definitions that specify the steps to take when managing the database.

Migrations are timestamped so that it is easy to apply migrations in the correct order and to rollback those migrations in reverse order if needs be. The migrations live in the db/migrate directory. Currently there are 4 migrations in there

20080925064318_create_sessions.rb       
20080925065056_create_blogs.rb
20080925064319_create_users.rb          
20080925065210_create_blog_entries.rb

The first is a fairly standard Rails migration that creates the session tables (run rake db:sessions:create to create this), the others are specific to this application. Each migration has a date-time as part of the file name and it's this name that determines the order in which the migrations are run. The 20080925065210_create_blog_entries.rb looks like this

class CreateBlogEntries < ActiveRecord::Migration
  def self.up
    create_table  :blog_entries do |t|
      t.string    :title,             :null => false
      t.text      :entry,             :null => false
      t.integer   :author_id,         :null => false
      t.datetime  :entry_added_date
      t.datetime  :entry_last_edited
      t.timestamps
    end
  end

  def self.down
    drop_table :blog_entries
  end
end

So it's a class that derives from ActiveRecord::Migration and provides two class methods (static methods to C#/C++ folks), up and down (it's the "self" that indicates that these are class methods and not instance methods). You can run the migration from the command line by using the Rake command

rake db:migrate

This runs any migrations that have not yet been run. How does it know which migrations to run? Tthere is a database table that holds the information about the migrations that have been run.

$ mysql -u root
mysql> use rblog_development
mysql> show tables;

shows something like

+-----------------------------+
| Tables_in_rblog_development |
+-----------------------------+
| blog_entries                |
| blogs                       |
| schema_migrations           |
| sessions                    |
| users                       |
+-----------------------------+

and

mysql> select * from schema_migrations;


+----------------+
| version        |
+----------------+
| 20080923152418 |
| 20080923152427 |
| 20080923152435 |
| 20080925064318 |
| 20080925064319 |
| 20080925065056 |
| 20080925065210 |
+----------------+

on my machine as I type this. Notice that the last entry in the table matches the datetime portion of the name of the last migration file.

When a migration is run (assuming it has not yet been added to the database), then the self.up method is executed. This method creates or modifies database entries. In the case of the blog_entries migration it creates the table and adds the eight columns from these five entries.

(title, entry, author_id, entry_added_date, entry_last_edited and timestamps

mysql shows this

mysql> show create table blog_entries;
+--------------+---------------------------------+
| Table        | Create Table                    |
+--------------+---------------------------------+
| blog_entries | CREATE TABLE `blog_entries` (
`id` int(11) NOT NULL auto_increment,
`title` varchar(255) NOT NULL,
`entry` text NOT NULL,
`author_id` int(11) NOT NULL,
`entry_added_date` datetime default NULL,
`entry_last_edited` datetime default NULL,
`created_at` datetime default NULL,
`updated_at` datetime default NULL,
PRIMARY KEY  (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1           |
+--------------+---------------------------------+

Notice that timestamps turns into two columns, and that an id column has been added as a primary key

A migration can also be rolled back. Running rake db:migrate rollback will rollback the last migration, or a specific version can be specified. For example rake db:migrate VERSION=20080925065056

Running the migrations this way runs the migrations in reverse order up to the specified migration, on the way the self.down mwthod of each migration is called. For the blog_entries migration that would drop the table. The down method should undo whatever the up method did!

One of the interesting (and frustrating) things about Rails is the way the migrations and the models work together. Running the script/generate scaffold blog_entry creates two files, the migration and the model. Looking in the model file there is ... nothing, just the class definition. The knowledge about the members of this class is in the migrations. This takes DRY (Do Not Repeat Yourself) to the limit but it can mean looking in several files (there maybe more than one migration per model) to find everything that the class uses. If the migrations get too "spread out", i.e. there are three or more migrations with modifications to one table then it is worth amalgamating those migrations into one file.


Posted Oct 10 2008, 03:37 PM by kevin-jones

Comments

Tim Ewald wrote re: Rails - Database Access
on 10-10-2008 1:04 PM

My last team did a lot with Rails. Here are some things we found with ActiveRecord...

- It was easier for us to update a set of migrations in place than to keep adding new ones. This reflected our build process and worked great.

- This part actually doesn't end up feeling that DRY once you start adding sizes to the DB to control use of space but also constraints to the model to enforce size limits before you go to the DB.

- If you start adding DB-specific stuff in your migrations, e.g., index generation and FK constraints, you end up moving your DB definition around using schemas instead of migrations, and you use a structure.sql file instead of schema.rb (there's a config param telling the system which to generate when you use rake to dump your schema).

Tim-

Add a Comment

(required)  
(optional)
(required)  
Remember Me?