where's my sql? designing databases with activerecord migrations
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.TRANSCRIPT
![Page 1: Where's My SQL? Designing Databases with ActiveRecord Migrations](https://reader033.vdocuments.mx/reader033/viewer/2022052821/55493cfdb4c9050a4d8b4d85/html5/thumbnails/1.jpg)
Where’s my SQL?Designing Databases with ActiveRecord Migrations
Eleanor McHughGames With Brains
![Page 2: Where's My SQL? Designing Databases with ActiveRecord Migrations](https://reader033.vdocuments.mx/reader033/viewer/2022052821/55493cfdb4c9050a4d8b4d85/html5/thumbnails/2.jpg)
the usual disclaimers
This presentation contains code
That code is probably broken
If that bothers you - fix it
It’s called a learning experience
![Page 3: Where's My SQL? Designing Databases with ActiveRecord Migrations](https://reader033.vdocuments.mx/reader033/viewer/2022052821/55493cfdb4c9050a4d8b4d85/html5/thumbnails/3.jpg)
RINDR - Rewriting bIND in Ruby
RailsMUD
Confidential Consultancy
so who the hell am I?Eleanor McHughGames With Brains
![Page 4: Where's My SQL? Designing Databases with ActiveRecord Migrations](https://reader033.vdocuments.mx/reader033/viewer/2022052821/55493cfdb4c9050a4d8b4d85/html5/thumbnails/4.jpg)
on the menu today
a basic development environment
ActiveRecord
Migrations
a simple plug-in
![Page 5: Where's My SQL? Designing Databases with ActiveRecord Migrations](https://reader033.vdocuments.mx/reader033/viewer/2022052821/55493cfdb4c9050a4d8b4d85/html5/thumbnails/5.jpg)
play along at home
you will need:
one development system
a standard Ruby install
the SQLite database
a current install of Rails
![Page 6: Where's My SQL? Designing Databases with ActiveRecord Migrations](https://reader033.vdocuments.mx/reader033/viewer/2022052821/55493cfdb4c9050a4d8b4d85/html5/thumbnails/6.jpg)
MacOS X
TextMate
kitchen table
chair
cups of tea
my dev environment
![Page 7: Where's My SQL? Designing Databases with ActiveRecord Migrations](https://reader033.vdocuments.mx/reader033/viewer/2022052821/55493cfdb4c9050a4d8b4d85/html5/thumbnails/7.jpg)
standard ruby install
visit http://www.ruby-lang.org
one-click installer for windows
source code for unix
RubyGems - http://rubygems.org/
![Page 8: Where's My SQL? Designing Databases with ActiveRecord Migrations](https://reader033.vdocuments.mx/reader033/viewer/2022052821/55493cfdb4c9050a4d8b4d85/html5/thumbnails/8.jpg)
SQLite 3
included with MacOS X :)
download from http://www.sqlite.org/
install SWiG? http://www.swig.org/
gem install sqlite3-ruby
![Page 9: Where's My SQL? Designing Databases with ActiveRecord Migrations](https://reader033.vdocuments.mx/reader033/viewer/2022052821/55493cfdb4c9050a4d8b4d85/html5/thumbnails/9.jpg)
Rails
gem install rails --include-dependencies
optional: gem install mongrel
![Page 10: Where's My SQL? Designing Databases with ActiveRecord Migrations](https://reader033.vdocuments.mx/reader033/viewer/2022052821/55493cfdb4c9050a4d8b4d85/html5/thumbnails/10.jpg)
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
![Page 11: Where's My SQL? Designing Databases with ActiveRecord Migrations](https://reader033.vdocuments.mx/reader033/viewer/2022052821/55493cfdb4c9050a4d8b4d85/html5/thumbnails/11.jpg)
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!”
![Page 12: Where's My SQL? Designing Databases with ActiveRecord Migrations](https://reader033.vdocuments.mx/reader033/viewer/2022052821/55493cfdb4c9050a4d8b4d85/html5/thumbnails/12.jpg)
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 }
![Page 13: Where's My SQL? Designing Databases with ActiveRecord Migrations](https://reader033.vdocuments.mx/reader033/viewer/2022052821/55493cfdb4c9050a4d8b4d85/html5/thumbnails/13.jpg)
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
);
![Page 14: Where's My SQL? Designing Databases with ActiveRecord Migrations](https://reader033.vdocuments.mx/reader033/viewer/2022052821/55493cfdb4c9050a4d8b4d85/html5/thumbnails/14.jpg)
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
);
![Page 15: Where's My SQL? Designing Databases with ActiveRecord Migrations](https://reader033.vdocuments.mx/reader033/viewer/2022052821/55493cfdb4c9050a4d8b4d85/html5/thumbnails/15.jpg)
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
![Page 16: Where's My SQL? Designing Databases with ActiveRecord Migrations](https://reader033.vdocuments.mx/reader033/viewer/2022052821/55493cfdb4c9050a4d8b4d85/html5/thumbnails/16.jpg)
Migrations
part of ActiveRecord
support iterative database development
expandable Data Definition Language
independent of database back-end
pure-Ruby solution :)
![Page 17: Where's My SQL? Designing Databases with ActiveRecord Migrations](https://reader033.vdocuments.mx/reader033/viewer/2022052821/55493cfdb4c9050a4d8b4d85/html5/thumbnails/17.jpg)
iterative development?
Ruby encourages agile methods
but SQL is far from agile...
changes to schema have side effects
risk of inconsistent state in database
![Page 18: Where's My SQL? Designing Databases with ActiveRecord Migrations](https://reader033.vdocuments.mx/reader033/viewer/2022052821/55493cfdb4c9050a4d8b4d85/html5/thumbnails/18.jpg)
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
![Page 19: Where's My SQL? Designing Databases with ActiveRecord Migrations](https://reader033.vdocuments.mx/reader033/viewer/2022052821/55493cfdb4c9050a4d8b4d85/html5/thumbnails/19.jpg)
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
![Page 20: Where's My SQL? Designing Databases with ActiveRecord Migrations](https://reader033.vdocuments.mx/reader033/viewer/2022052821/55493cfdb4c9050a4d8b4d85/html5/thumbnails/20.jpg)
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
![Page 21: Where's My SQL? Designing Databases with ActiveRecord Migrations](https://reader033.vdocuments.mx/reader033/viewer/2022052821/55493cfdb4c9050a4d8b4d85/html5/thumbnails/21.jpg)
...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
![Page 22: Where's My SQL? Designing Databases with ActiveRecord Migrations](https://reader033.vdocuments.mx/reader033/viewer/2022052821/55493cfdb4c9050a4d8b4d85/html5/thumbnails/22.jpg)
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
![Page 23: Where's My SQL? Designing Databases with ActiveRecord Migrations](https://reader033.vdocuments.mx/reader033/viewer/2022052821/55493cfdb4c9050a4d8b4d85/html5/thumbnails/23.jpg)
fun with plug-ins
playing with DDLs is fun
but what about the data model?
plug-ins are great for this
![Page 24: Where's My SQL? Designing Databases with ActiveRecord Migrations](https://reader033.vdocuments.mx/reader033/viewer/2022052821/55493cfdb4c9050a4d8b4d85/html5/thumbnails/24.jpg)
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
![Page 25: Where's My SQL? Designing Databases with ActiveRecord Migrations](https://reader033.vdocuments.mx/reader033/viewer/2022052821/55493cfdb4c9050a4d8b4d85/html5/thumbnails/25.jpg)
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
![Page 26: Where's My SQL? Designing Databases with ActiveRecord Migrations](https://reader033.vdocuments.mx/reader033/viewer/2022052821/55493cfdb4c9050a4d8b4d85/html5/thumbnails/26.jpg)
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
![Page 27: Where's My SQL? Designing Databases with ActiveRecord Migrations](https://reader033.vdocuments.mx/reader033/viewer/2022052821/55493cfdb4c9050a4d8b4d85/html5/thumbnails/27.jpg)
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
![Page 28: Where's My SQL? Designing Databases with ActiveRecord Migrations](https://reader033.vdocuments.mx/reader033/viewer/2022052821/55493cfdb4c9050a4d8b4d85/html5/thumbnails/28.jpg)
conclusions?
Migrations simplify data definition
plug-ins simplify model definition
together they open many possibilities