A presentation given at RoReXchange in February 2007. Covers some abuses of the ActiveRecord Migrations mechanism along with examples of simple Rails plug-in design.
2. the usual disclaimers
This presentation contains code
That code is probably broken
If that bothers you - fix it
It’s called a learning experience
3. so who the hell am I?
Eleanor McHugh
Games With Brains
eleanor@games-with-brains.com
RINDR - Rewriting bIND in Ruby
RailsMUD
Confidential Consultancy
4. on the menu today
a basic development environment
ActiveRecord
Migrations
a simple plug-in
5. play along at home
you will need:
one development system
a standard Ruby install
the SQLite database
a current install of Rails
10. ActiveRecord?
it’s an Object-Relational Mapper
Ruby objects are database tables
their attributes are table columns
each instance is a table row
can be used separately from Rails
gem install activerecord
11. using ActiveRecord
intuitive to use
supports popular database backends
rails generators for the lazy
require “rubygems”
gem “activerecord”
ActiveRecord::Base.establish_connection :adapter => “sqlite3”, :database => “test.db”
class User < ActiveRecord::Base
validates_presence_of :name
validates_uniqueness_of
validates_presence_of :password
end
user = User.create :name => “Eleanor McHugh”, :password => “like_duh!”
12. are tables related?
it wouldn’t be relational if they weren’t!
and here’s an example to prove it...
class User < ActiveRecord::Base
validates_presence_of :name
validates_uniqueness_of
validates_presence_of :password
has_and_belongs_to_many :roles
end
class Role < ActiveRecord::Base
has_and_belongs_to_many :users
validates_presence_of :name
end
Role.create :name => “admin”
User.create :name => “Eleanor McHugh”, :password => “like_duh!”
User.find_by_name(“Eleanor McHugh).roles << Role.find_by_name(“admin”)
Role.find_by_name(“admin”).users.each { |user| puts user.name }
13. but what about tables?
assumed to exist in your database
class User maps to table users
attribute name maps to column name
each instance of User has a unique id
create table users(
id int unsigned not null auto_increment primary key,
name varchar(40) not null,
password varchar(16) not null
);
14. in this case...
roles_users is a join table
join tables don’t have id columns
create table users(
id int unsigned not null auto_increment primary key,
name varchar(40) not null,
password varchar(16) not null
);
create table roles(
id int unsigned not null auto_increment primary key,
name varchar(40) not null
);
create table roles_users(
users_id int not null,
roles_id int not null
);
15. reasons to be tearful
tables defined in SQL schema
have to manually insert the id column
probably database dependent
would much prefer a Ruby solution
16. Migrations
part of ActiveRecord
support iterative database development
expandable Data Definition Language
independent of database back-end
pure-Ruby solution :)
17. iterative development?
Ruby encourages agile methods
but SQL is far from agile...
changes to schema have side effects
risk of inconsistent state in database
18. the basic DDL
in Ruby and database independent :)
only two methods: up and down
class AddUserTable < ActiveRecord::Migration
def self.up
create_table :roles, :force => true { |t| t.column :name, :string, :null => false }
create_table :users, :force => true do |t|
[:name, :full_name].each { |c| t.column c, :string, :null => false }
[:email_address_id, :account_status_code_id].each { |c| t.column c, :integer, :null => false }
[:profile_id, :stylesheet_id].each { |c| t.column c, :integer }
[:password_hash, :password_salt].each { |c| t.column c, :string, :null => false }
end
create_table :roles_users, :force => true do |t|
[:role_id, :user_id].each { |c| t.column c, :integer, :null => false }
end
[:name, :full_name, :account_status_code_id].each { |c| add_index :users, c }
add_index :roles, :name
[:role_id, :user_id].each { |c| add_index :roles_users, c }
end
def self.down
[:role_id, :user_id].each { |c| remove_index :roles_users, c }
remove_index :roles, :name
[:name, :full_name, :account_status_code_id].each { |c| remove_index :users, c }
[:roles_users, :roles, :users].each { |t| drop_table t }
end
end
19. let’s rev up the pace...
table definitions could be more succinct
# file ‘enhanced_table_definitions.rb’
# inspired by Hobo
module ActiveRecord::ConnectionAdapters
class TableDefinition
@@known_column_types = [:integer, :float, :decimal, :datetime, :date, :timestamp, :time, :text, :string, :binary, :boolean ]
def foreign_key foreign_table, *args
column foreign_key_name_for(foreign_table).to_sym, :integer, take_options!(args)
end
def method_missing name, *args
@@known_column_types.include?(name) ? args.each {|type| column type, name, take_options!(args) } : super
end
def self.foreign_key_name_for table
quot;#{Inflector.singularize(table)}_idquot;
end
private
def take_options!(args)
args.last.is_a?(Hash) ? args.pop : {}
end
end
end
20. let’s rev up the pace...
# file ‘expanded_ddl.rb’
require ‘enhanced_table_definitions’
module ExpandedDDL
def create_timestamped_table table, options = {}
create_table table, :force => !options[:no_force] do |t|
[:created_at, :modified_at].each { |c| t.datetime c }
yield t if block_given?
end
[:created_at, :modified_at].each { |c| add_index table, c }
end
def drop_timestamped_table table
[:created_at, :modified_at].each { |c| remove_index table, c }
drop_table table
end
def create_join_table primary_table, secondary_table, options = {}
table = join_table_name(primary_table, secondary_table)
create_timestamped_table(table, options) { |t| t.foreign_key key, :null => false }
[primary_key, secondary_key].each { |c| add_foreign_key_index table, c }
end
def drop_join_table primary_table, secondary_table
table = join_table_name(primary_table, secondary_table)
[primary_table, secondary_table].each { |c| remove_foreign_key_index table, c }
drop_table table
end
def add_foreign_key_index table, key, options = {}
add_index table, foreign_key_name_for(key), options
end
def remove_foreign_key_index table, key
remove_index table, foreign_key_name_for(key)
end
def join_table_name primary_table, secondary_table
(primary_table.to_s < secondary_table.to_s) ? quot;#{primary_table}_#{secondary_table}quot; : quot;#{secondary_table}_#{primary_table}quot;
end
def foreign_key_name_for table
“#{Inflector.singularize(table)}_id”
end
end
22. run in sequence
rake db:migrate
each migration has a sequence number
migrations are run in this order
a hypothetical example from RailsMUD
001_add_account_tables
002_add_character_tables
003_add_action_tables
004_alter_account_tables
005_add_creature_tables
23. fun with plug-ins
playing with DDLs is fun
but what about the data model?
plug-ins are great for this
24. what’s in a name?
this plug-in adds names to a model
ActiveRecord::Base.send(:include, ActiveRecord::Acts::Named)
module ActiveRecord
module Acts #:nodoc:
module Named #:nodoc:
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
def acts_as_named(options = {})
write_inheritable_attribute(:acts_as_named_options, {:from => options[:from]})
class_inheritable_reader :acts_as_named_options
validates_presence_of :name
validates_uniqueness_of :name unless options[:duplicate_names]
include ActiveRecord::Acts::Named::InstanceMethods
extend ActiveRecord::Acts::Named::SingletonMethods
end
end
module SingletonMethods
end
module InstanceMethods
end
end
end
end
25. some DDL goodies
module ExpandedDDL
=begin
add the following to the module
=end
def create_named_table table, options
create_timestamped_table table, take_options!(options) do |t|
t.column :name, :string, :null => false
yield t if block_given?
end
add_index table, :name, :unique => !options[:duplicate_names_allowed]
end
def drop_named_table table
remove_index table, :name
drop_table table
end
end
27. and its model
this model invokes acts_as_named
the default is for unique names only
:duplicate_names =>true
indices in the DDL become relations
class User < ActiveRecord::Base
acts_as_named
validates_presence_of :full_name
belongs_to :account_status_code
validates_presence_of :account_status_code
has_one :profile
has_one :stylesheet
has_one :email_address
validates_presence_of :email_address
has_and_belongs_to_many :roles
end