Upload
eleanor-mchugh
View
4.234
Download
3
Embed Size (px)
DESCRIPTION
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.
Citation preview
Where’s my SQL?Designing Databases with ActiveRecord Migrations
Eleanor McHughGames With Brains
the usual disclaimers
This presentation contains code
That code is probably broken
If that bothers you - fix it
It’s called a learning experience
RINDR - Rewriting bIND in Ruby
RailsMUD
Confidential Consultancy
so who the hell am I?Eleanor McHughGames With Brains
on the menu today
a basic development environment
ActiveRecord
Migrations
a simple plug-in
play along at home
you will need:
one development system
a standard Ruby install
the SQLite database
a current install of Rails
MacOS X
TextMate
kitchen table
chair
cups of tea
my dev environment
standard ruby install
visit http://www.ruby-lang.org
one-click installer for windows
source code for unix
RubyGems - http://rubygems.org/
SQLite 3
included with MacOS X :)
download from http://www.sqlite.org/
install SWiG? http://www.swig.org/
gem install sqlite3-ruby
Rails
gem install rails --include-dependencies
optional: gem install mongrel
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
using ActiveRecordintuitive 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::Basevalidates_presence_of :namevalidates_uniqueness_ofvalidates_presence_of :password
end
user = User.create :name => “Eleanor McHugh”, :password => “like_duh!”
are tables related?it wouldn’t be relational if they weren’t!
and here’s an example to prove it...
class User < ActiveRecord::Basevalidates_presence_of :namevalidates_uniqueness_ofvalidates_presence_of :passwordhas_and_belongs_to_many :roles
end
class Role < ActiveRecord::Basehas_and_belongs_to_many :usersvalidates_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 }
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
);
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
);
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
Migrations
part of ActiveRecord
support iterative database development
expandable Data Definition Language
independent of database back-end
pure-Ruby solution :)
iterative development?
Ruby encourages agile methods
but SQL is far from agile...
changes to schema have side effects
risk of inconsistent state in database
the basic DDLin Ruby and database independent :)
only two methods: up and downclass AddUserTable < ActiveRecord::Migration
def self.upcreate_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 }
endcreate_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 }
endend
let’s rev up the pace...table definitions could be more succinct
# file ‘enhanced_table_definitions.rb’# inspired by Hobo
module ActiveRecord::ConnectionAdaptersclass TableDefinition
@@known_column_types = [:integer, :float, :decimal, :datetime, :date, :timestamp, :time, :text, :string, :binary, :boolean ]
def foreign_key foreign_table, *argscolumn 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"#{Inflector.singularize(table)}_id"
end
privatedef take_options!(args)
args.last.is_a?(Hash) ? args.pop : {}end
endend
let’s rev up the pace...# file ‘expanded_ddl.rb’require ‘enhanced_table_definitions’
module ExpandedDDLdef 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_tabletable = 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, keyremove_index table, foreign_key_name_for(key)
end
def join_table_name primary_table, secondary_table(primary_table.to_s < secondary_table.to_s) ? "#{primary_table}_#{secondary_table}" : "#{secondary_table}_#{primary_table}"
end
def foreign_key_name_for table“#{Inflector.singularize(table)}_id”
endend
...and see the benefitsrequire ‘expanded_ddl’
class AddUserTable < ActiveRecord::Migrationextend ExpandedDDL
def self.upcreate_timestamped_table :users, :force => true do |t|
[:name, :full_name].each { |c| t.string c, :null => false }[:email_addresses, :account_status_codes].each { |key| t.foreign_key key, :null => false }[:profiles, :stylesheets].each { |key| t.foreign_key key }[:password_hash, :password_salt].each { |c| t.string c, :null => false }
endadd_index :users, :name, :unique => trueadd_index :users, :full_nameadd_foreign_key_index :users, :account_status_codes
create_table :roles, :force => true { |t| t.column :name, :string, :null => false }add_index :roles, :name
create_join_table :roles, :users, :force => true[: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 }drop_join_table :roles, :usersremove_index :roles, :nameremove_foreign_key_index :users, :account_status_codes[:name, :full_name].each { |c| remove_index :users, c }[:roles, :users].each { |t| drop_timestamped_table t }
endend
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_tables002_add_character_tables003_add_action_tables004_alter_account_tables005_add_creature_tables
fun with plug-ins
playing with DDLs is fun
but what about the data model?
plug-ins are great for this
what’s in a name?this plug-in adds names to a modelActiveRecord::Base.send(:include, ActiveRecord::Acts::Named)
module ActiveRecordmodule Acts #:nodoc:
module Named #:nodoc:def self.included(base)
base.extend(ClassMethods)end
module ClassMethodsdef acts_as_named(options = {})
write_inheritable_attribute(:acts_as_named_options, {:from => options[:from]})class_inheritable_reader :acts_as_named_optionsvalidates_presence_of :namevalidates_uniqueness_of :name unless options[:duplicate_names]
include ActiveRecord::Acts::Named::InstanceMethodsextend ActiveRecord::Acts::Named::SingletonMethods
endend
module SingletonMethodsend
module InstanceMethodsend
end end
end
some DDL goodies
module ExpandedDDL=begin
add the following to the module=end
def create_named_table table, optionscreate_timestamped_table table, take_options!(options) do |t|
t.column :name, :string, :null => falseyield t if block_given?
endadd_index table, :name, :unique => !options[:duplicate_names_allowed]
end
def drop_named_table tableremove_index table, :namedrop_table table
endend
an updated migrationrequire ‘expanded_ddl’
class AddUserTable < ActiveRecord::Migrationextend ExpandedDDL
def self.upcreate_named_table :roles, :force => truecreate_named_table :users, :force => true do |t|
t.string :full_name, :null => false[:email_addresses, :account_status_codes].each { |key| t.foreign_key key, :null => false }[:profiles, :stylesheets].each { |key| t.foreign_key key }[:password_hash, :password_salt].each { |c| t.string c, :null => false }
endadd_index :users, :full_nameadd_foreign_key_index :users, :account_status_codescreate_join_table :roles, :users, :force => true[: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 }drop_join_table :roles, :usersremove_foreign_key_index :users, :account_status_codesremove_index :users, :full_name[:roles, :users].each { |t| drop_named_table t }
endend
and its modelthis model invokes acts_as_named
the default is for unique names only
:duplicate_names =>true
indices in the DDL become relationsclass User < ActiveRecord::Base
acts_as_namedvalidates_presence_of :full_namebelongs_to :account_status_codevalidates_presence_of :account_status_codehas_one :profilehas_one :stylesheethas_one :email_addressvalidates_presence_of :email_addresshas_and_belongs_to_many :roles
end
conclusions?
Migrations simplify data definition
plug-ins simplify model definition
together they open many possibilities