Wednesday, August 29, 2007

ActiveRecord Migration: how to write and the results

Recently, I started learning Ruby on Rails which is getting more and more popular than a Java based web application framework. Ideas and cultures are quite different from those of Java, so I have struggled to understand them. What I figured out first was that Ruby on Rails programming is tightly coupled to database tables. Creating a table in a database is the first step of every programming. To cope with a database, Rails provides us a tool, ActiveRecord, to connect to the database, and to create tables in the database. ActiveRecord enables us to migrate from a table definition written by ruby to the database schema. This is the memo how a migration can be defined and what tables in databases I got as a result of the migration.

When I wrote a database table definition for a migration, I referred to the web pages listed below.
For example, from this table definition in SQL, I defined following ruby file.
CREATE TABLE items (
id smallint(5) unsigned NOT NULL auto_increment,
done tinyint(1) unsigned NOT NULL default '0',
priority tinyint(1) unsigned NOT NULL default '3',
description varchar(40) NOT NULL default '',
due_date date default NULL,
category_id smallint(5) unsigned NOT NULL default '0',
node_id smallint(5) unsigned default NULL,
private tinyint(3) unsigned NOT NULL default '0',
created_on timestamp(14) NOT NULL,
updated_on timestamp(14) NOT NULL,
PRIMARY KEY (id)
) TYPE=MyISAM COMMENT='List of items to be done';

class CreateItems < ActiveRecord::Migration
def self.up
create_table :items do |t|
t.column :done, :integer, :null => false, :default => 0
t.column :priority, :integer, :null => false, :default => 3
t.column :description, :string, :limit => 40, :null => false, :default =>''
t.column :due_date, :date, :null => true, :default => nil
t.column :category_id, :integer, :null => false, :default => 0
t.column :note_id, :integer, :null => true, :default => nil
t.column :private, :integer, :null => false, :default => 0
t.column :created_on, :timestamp, :null => false
t.column :updated_on, :timestamp, :null => false
end
end

def self.down
drop_table :items
end
end
According to the API documentation of ActiveRecord::ConnectionAdapters::TableDefinition, data type must be one of primary_key, string, text, integer, float, decimal, datetime, timestamp, time, date, binary, and boolean. Therefore, I mapped both smallint and tinyint types to a integer type. I could write the size of integer precisely by adding :limit, but I didn't do that. There's no unsigned integer type, and I have to care not to have negative values.

What I didn't mapped to the ruby file at all was “TYPE=MyISAM COMMENT='List of items to be done';” in the last line of SQL. I could have written like this:
create_table :items, :options => 'engine=MyISAM' do |t|
However, this MySQL specific parameter doesn't have any special effect because my DBMS is Apache Derby that comes with NetBeans.

Here's another mapping example. This shows how I can define UNIQUE KEY in a ruby file.
CREATE TABLE 'categories' (
id smallint(5) unsigned NOT NULL auto_increment,
category varchar(20) NOT NULL default ''
created_on timestamp(14) NOT NULL,
updated_on timestamp(14) NOT NULL,
PRIMARY KEY ('id'),
UNIQUE KEY 'category_key' ('category')
) TYPE=MyISAM COMMENT='List of categories'

class CreateCategories < ActiveRecord::Migration
def self.up
create_table :categories do |t|
t.column :category, :string, :limit => 20, :default => '', :null => false
t.column :created_on, :timestamp, :null => false
t.column :updated_on, :timestamp, :null => false
end
add_index :categories, :category, :unique => true, :name => 'category_key'
end

def self.down
drop_table :categories
end
end

Executing migration is the next step after defining the database table in ruby. Before doing it, I edited config/databse.yml and config/environment.rb to set up a connection to the database as follows:
databse.yml(exerpt)
development:
adapter: jdbc
driver: org.apache.derby.jdbc.ClientDriver
url: jdbc:derby://localhost:1527/todos
username: foo
password: bar
encoding: utf8

evironment.rb(excerpt)
RAILS_GEM_VERSION = '1.2.3' unless defined? RAILS_GEM_VERSION

require File.join(File.dirname(__FILE__), 'boot')

if RUBY_PLATFORM =~ /java/
require 'rubygems'
RAILS_CONNECTION_ADAPTERS = %w(jdbc)
end

Rails::Initializer.run do |config|
end

To make it happen, I chose “Migrate Database” from menus provided by NetBeans IDE(Go to the Projects window, right click on the project name, choose“Migrate Database” from a popup menu then select “To Current Version”). This is equivalent to run “rake db:migrate” command. NetBeans showed some messages on Output window, and create the table in the database. Though I can see what table was created on NetBeans, I dared check it interactively by running ij command of Apache Derby because outputs in interactive mode was more familiar than properties in the
popup window.

$ java -jar lib/derbyrun.jar ij
ij> connect 'jdbc:derby://localhost:1527/todos' user 'foo' password 'bar';
ij> describe items;
COLUMN_NAME |TYPE_NAME|DEC&|NUM&amp;amp;|COLUM&|COLUMN_DEF|CHAR_OCTE&|IS_NULL&
------------------------------------------------------------------------------
ID |INTEGER |0 |10 |10 |GENERATED&|NULL |NO
DONE |INTEGER |0 |10 |10 |0 |NULL |NO
PRIORITY |INTEGER |0 |10 |10 |3 |NULL |NO
DESCRIPTION |VARCHAR |NULL|NULL|40 |'' |80 |NO
DUE_DATE |DATE |0 |10 |10 |NULL |NULL |YES
CATEGORY_ID |INTEGER |0 |10 |10 |0 |NULL |NO
NOTE_ID |INTEGER |0 |10 |10 |NULL |NULL |YES
PRIVATE |INTEGER |0 |10 |10 |0 |NULL |NO
CREATED_ON |TIMESTAMP|6 |10 |26 |NULL |NULL |NO
UPDATED_ON |TIMESTAMP|6 |10 |26 |NULL |NULL |NO

ij> describe categories;
COLUMN_NAME |TYPE_NAME|DEC&|NUM&amp;amp;|COLUM&|COLUMN_DEF|CHAR_OCTE&|IS_NULL&
------------------------------------------------------------------------------
ID |INTEGER |0 |10 |10 |GENERATED&|NULL |NO
CATEGORY |VARCHAR |NULL|NULL|20 |'' |40 |NO
CREATED_ON |TIMESTAMP|6 |10 |26 |NULL |NULL |NO
UPDATED_ON |TIMESTAMP|6 |10 |26 |NULL |NULL |NO

ij> show indexes from categories;
TABLE_NAME |COLUMN_NAME |NON_U&|TYPE|ASC&amp;amp;|CARDINA&|PAGES
----------------------------------------------------------------------------
CATEGORIES |CATEGORY |0 |3 |A |NULL |NULL
CATEGORIES |ID |0 |3 |A |NULL |NULL

For comparison, I tried the migration in the same way by using well-used MySQL. On MySQL, the table was created like this:
mysql> describe items;
+-------------+-------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-------------+-------------+------+-----+---------+----------------+
| id | int(11) | NO | PRI | NULL | auto_increment |
| done | int(11) | NO | | 0 | |
| priority | int(11) | NO | | 3 | |
| description | varchar(40) | NO | | NULL | |
| due_date | date | YES | | NULL | |
| category_id | int(11) | NO | | 0 | |
| note_id | int(11) | YES | | NULL | |
| private | int(11) | NO | | 0 | |
| created_on | datetime | NO | | NULL | |
| updated_on | datetime | NO | | NULL | |
+-------------+-------------+------+-----+---------+----------------+
10 rows in set (0.01 sec)

We can see small differences in length of an integer type, a mapping of a timestamp type and default value of a varchar type.

Every time I try to run Rails samples, I'm often puzzled how I can write schema definitions in ruby and how the migration works from definitions to real tables on database. This memo would help me not to be confused next time.

No comments: