Where's My SQL? Designing Databases with ActiveRecord Migrations
Upcoming SlideShare
Loading in...5
×
 

Where's My SQL? Designing Databases with ActiveRecord Migrations

on

  • 7,629 views

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.

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.

Statistics

Views

Total Views
7,629
Views on SlideShare
7,614
Embed Views
15

Actions

Likes
2
Downloads
133
Comments
0

1 Embed 15

http://www.slideshare.net 15

Accessibility

Categories

Upload Details

Uploaded via as Adobe PDF

Usage Rights

© All Rights Reserved

Report content

Flagged as inappropriate Flag as inappropriate
Flag as inappropriate

Select your reason for flagging this presentation as inappropriate.

Cancel
  • Full Name Full Name Comment goes here.
    Are you sure you want to
    Your message goes here
    Processing…
Post Comment
Edit your comment

Where's My SQL? Designing Databases with ActiveRecord Migrations Where's My SQL? Designing Databases with ActiveRecord Migrations Presentation Transcript

  • Where’s my SQL? Designing Databases with ActiveRecord Migrations Eleanor McHugh Games 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
  • so who the hell am I? Eleanor McHugh Games With Brains eleanor@games-with-brains.com RINDR - Rewriting bIND in Ruby RailsMUD Confidential Consultancy
  • 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
  • my dev environment MacOS X TextMate kitchen table chair cups of tea
  • 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 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!”
  • 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 }
  • 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 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
  • 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
  • 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
  • ...and see the benefits require ‘expanded_ddl’ class AddUserTable < ActiveRecord::Migration extend ExpandedDDL def self.up create_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 } end add_index :users, :name, :unique => true add_index :users, :full_name add_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, :users remove_index :roles, :name remove_foreign_key_index :users, :account_status_codes [:name, :full_name].each { |c| remove_index :users, c } [:roles, :users].each { |t| drop_timestamped_table t } end end
  • 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
  • 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 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
  • 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
  • an updated migration require ‘expanded_ddl’ class AddUserTable < ActiveRecord::Migration extend ExpandedDDL def self.up create_named_table :roles, :force => true create_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 } end add_index :users, :full_name add_foreign_key_index :users, :account_status_codes 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, :users remove_foreign_key_index :users, :account_status_codes remove_index :users, :full_name [:roles, :users].each { |t| drop_named_table t } end end
  • 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
  • conclusions? Migrations simplify data definition plug-ins simplify model definition together they open many possibilities