where's my sql? designing databases with activerecord migrations

28
Where’s my SQL? Designing Databases with ActiveRecord Migrations Eleanor McHugh Games With Brains

Upload: eleanor-mchugh

Post on 06-May-2015

4.234 views

Category:

Technology


3 download

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

Where’s my SQL?Designing Databases with ActiveRecord Migrations

Eleanor McHughGames With Brains

Page 2: Where's My SQL? Designing Databases with ActiveRecord Migrations

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

RINDR - Rewriting bIND in Ruby

RailsMUD

Confidential Consultancy

so who the hell am I?Eleanor McHughGames With Brains

[email protected]

Page 4: Where's My SQL? Designing Databases with ActiveRecord Migrations

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

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

MacOS X

TextMate

kitchen table

chair

cups of tea

my dev environment

Page 7: Where's My SQL? Designing Databases with ActiveRecord Migrations

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

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

Rails

gem install rails --include-dependencies

optional: gem install mongrel

Page 10: Where's My SQL? Designing Databases with ActiveRecord Migrations

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

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

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

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

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

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

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

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

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

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

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

...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

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

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

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

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

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

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

conclusions?

Migrations simplify data definition

plug-ins simplify model definition

together they open many possibilities