700 likes | 851 Views
Taking ActiveRecord to the Next Level. Blythe Dunham blythe@snowgiraffe.com http://snowgiraffe.com. Goal. Leverage advanced MySQL functionality with ActiveRecord. Disclaimer!!!!. Premature Optimization. ActiveRecord 101. What's going on under the covers?. ActiveRecord 101. Active
E N D
Taking ActiveRecord to the Next Level • Blythe Dunham • blythe@snowgiraffe.com • http://snowgiraffe.com
Goal • Leverage advanced MySQL functionality • with ActiveRecord
Disclaimer!!!! Premature Optimization
ActiveRecord 101 • What's going on under the covers?
ActiveRecord 101 Active Record Model Database Table ff
ActiveRecord 101 with Animals! Active Record Model Database Table ff
Creating a Record • animal = Animal.new • animal.name = 'Jerry Giraffe' • animal.password = 'jerry' • animal.save! • INSERT INTO `animals` • (`name`, `updated_at`, `species_id`, • `password`, `email`, `created_at`) • VALUES('Jerry Giraffe', '2009-03-15 00:48:28', • NULL, 'jerry', NULL, '2009-03-15 00:48:28')
Updating a Record • animal.name = 'Jerry G' • animal.save! • UPDATE `animals` • SET `updated_at` = '2009-03-15 03:01:06', • `name` = 'Jerry G' • WHERE `id` = 1
Finding a Record • jerry = Animal.find :first, • :conditions => ['name = ?', 'Jerry G'] • SELECT * FROM `animals` • WHERE (name = 'Jerry G') LIMIT 1 #shortcut Animal.find_by_name 'Jerry G'
Representing Relationships Animal name email password fav_beer updated_at created_at species_id Species name
Representing Relationships(DDL) CREATE TABLE `animals` ( `id` int(11) NOT NULL auto_increment, `name` varchar(35) NOT NULL, `email` varchar(40) default NULL, `fav_beer` enum('Guiness','Pabst','Redhook','Chimay') default 'Pabst', `created_at` datetime default NULL, `updated_at` datetime default NULL, `password` varchar(25) character set latin1 collate latin1_bin NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 CREATE TABLE `species` ( `id` int(11) NOT NULL auto_increment, `name` varchar(255), PRIMARY KEY (`id`),) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8
Representing Relationships (AR) class Animal < ActiveRecord::Base belongs_to :species end class Species < ActiveRecord::Base has_many :animals end
Representing Relationships (AR) jerry.species SELECT * FROM `species` WHERE (`species`.`id` = 1) species.animals SELECT * FROM `animals` WHERE (`animals`.species_id = 1)
Representing Relationships (AR) giraffe = Species.find_by_name 'giraffe' giraffe.animals << jerry SELECT * FROM `species` WHERE (`species`.`name` = 'giraffe' ) LIMIT 1 UPDATE `animals` SET `species_id` = 1, `updated_at` = '2009-03-19 23:15:54' WHERE `id` = 7
Migration • Set limits • Set default values • Identify NOT NULL columns • Use enumerated columns • Custom DDL • Add (unique) indices • Foreign Keys are great • Primary Key Modifications
Migration 101 ruby script/generate scaffold Animal name:string password:string email:string fav_beer:string class CreateAnimals < ActiveRecord::Migration def self.up create_table :animals do |t| t.string :name t.string :password t.string :email t.string :fav_beer t.timestamps end end def self.down drop_table :animals end end
Set Limits Default String is VARCHAR(255) create_table :animals do |t| t.string :name, :limit => 35 t.string :password, :limit => 25 t.string :email, :limit => 40 t.string :fav_beer, :limit => 40 t.timestamps end
Numeric Type Limits t.integer :mysmallint, :limit => 2 "Smart types" determines numeric type for MySQL
Set columns to NOT NULL create_table :animals do |t| t.string :name, :limit => 35, :null => false t.string :password, :limit => 25, :null => false t.string :email, :limit => 40 t.string :fav_beer, :limit => 40 t.timestamps end
Set default values create_table :animals do |t| t.string :name, :limit => 35, :null => false t.string :password, :limit => 25, :null => false t.string :email, :limit => 40, :default => nil t.string :fav_beer, :limit => 40 :default => 'Pabst' t.timestamps end
Remove unneeded columns create_table :animals do |t| t.string :name, :limit => 35, :null => false t.string :password, :limit => 25, :null => false t.string :email, :limit => 40, :default => nil t.string :fav_beer, :limit => 40 :default => 'Pabst' t.timestamps end
Enumerated Column Plugin create_table :animals do |t| t.string :name, :limit => 35, :null => false t.string :password, :limit => 25, :null => false t.string :email, :limit => 40, :default => nil t.enum :fav_beer, :default => 'Pabst' :limit => %w(Chimay Pabst Redhook) t.timestamps end
Think about the table parameters create_table :animals, :options => 'ENGINE=MyISAM' do |t| t.string :name, :limit => 35, :null => false t.string :password, :limit => 25, :null => false t.string :email, :limit => 40, :default => nil t.enum :fav_beer, :default => nil :limit => %w(Chimay Pabst Redhook) t.timestamps end
Custom DDL create_table :animals do |t| t.string :name, :limit => 35, :null => false t.string :email, :limit => 40, :default => nil t.enum :fav_beer, :default => nil :limit => %w(Chimay Pabst Redhook) t.timestamps end #case sensitive password (encrypted) execute "ALTER TABLE `animals` ADD `password` varchar(25) character set latin1 collate latin1_bin NOT NULL"
Create (Unique) Indices create_table :species do |t| t.string :name, :null => false, :limit => 25 end add_index :species, :name, :unique => true, :name => 'uk_species_name'
ActiveRecord Uniqueness • class Species < ActiveRecord::Base • validates_uniqueness_of :name • end • Doesn't Guaranty Data Integrity!
I Heart Foreign Keys • Referential Integrity
The AR Way: Foreign Keys • class Species < ActiveRecord::Base • has_many :animals, :dependent => :nullify • end
The Rails Way: Foreign Keys • class Species < ActiveRecord::Base • has_many :animals, :dependent => :nullify • end • Dependent Value SQL Equivalent: • :nullify => ON DELETE SET NULL • :delete_all => ON DELETE CASCADE • :destroy => No SQL equivalent. Every association is instantiated and and callbacks are executed before destruction
Redhills Foreign Key Migration Plugin to the rescue! • add_column :animals, :species_id, :integer, • :references => :species, • :name => 'fk_animal_species', • :on_delete => :set_null, • :on_update => :cascade • ALTER TABLE `animals` ADD `species_id` int(11); • ALTER TABLE animals ADD CONSTRAINT fk_animal_species FOREIGN KEY (species_id) REFERENCES species (id) ON UPDATE CASCADE ON DELETE SET NULL;
Primary Keys CREATE TABLE `animals` ( `id` int(11) NOT NULL auto_increment, `name` varchar(35) NOT NULL, `email` varchar(40) default NULL, `fav_beer` enum('Guiness','Pabst','Redhook','Chimay') default 'Pabst', `species_id` int(11) default NULL, `created_at` datetime default NULL, `updated_at` datetime default NULL, `password` varchar(25) character set latin1 collate latin1_bin NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 CREATE TABLE `species` ( `id` int(11) NOT NULL auto_increment, `name` varchar(255), PRIMARY KEY (`id`),) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8
Modify the Rails Primary Key Change type with change_column MySQL Migration Optimization Plugin create_table :animals, :primary_key => "special_key", :primary_column => { :type => :integer, :limit => 2, :precision => :unsigned, :scale => 3 } do |t| end CREATE TABLE `animals` (`special_key` smallint(3) UNSIGNED NOT NULL auto_increment PRIMARY KEY) ENGINE=InnoDB;
Advanced ActiveRecord • Insert and update options • Import bulk data • Finder Options • Tools: plugin AR-Extensions
ActiveRecord on Steroids: ar-extensions plugin • Additional Create and Update options • save(options={}) • save!(options={}) • create(args, options={}) • create!(args, options={}) • Options • :ignore • :on_duplicate_key_update • :keywords • :reload • :pre_sql • :post_sql
:ignore • Standard ActiveRecord: • Create the record if it does not already exist • unless Animal.find_by_name('Jerry G') • Animal.create!(:name => 'Jerry G', • :password => 'jerry') • end
:ignore • Ignore duplicates! One query, less code, fewer queries! • Animal.create!({:name => 'Jerry G', • :password => 'jerry'}, • {:ignore => true})
:on_duplicate_key_update • Update the record if it exists, if not create a new one. • A lot of code to upsert and performs two SQL queries! • jerry = Animal.find_by_name 'Jerry G' • jerry ||= Animal.new(:name => 'Jerry G') • jerry.password = 'frenchfry' • jerry.save!
:on_duplicate_key_update • jerry = Animal.new :name => 'Jerry G', • :password => 'frenchfry' • jerry.save! :on_duplicate_key_update => • [:password, :updated_at] • INSERT INTO animals • (`name`, `updated_at`, `species_id`, • `password`,`email`, `created_at`) • VALUES('Jerry G', '2009-03-15 06:17:51', NULL, • 'frenchfry', NULL, '2009-03-15 06:17:51') • ON DUPLICATE KEY UPDATE • `animals`.`password`=VALUES(`password`), • `animals`.`updated_at`=VALUES(`updated_at`)
Reloading the instance • AR Data can become inconsistent with DB after an IGNORE, UPDATE, or ON DUPLICATE KEY UPDATE • reload executes more queries • For UPDATE the duplicate is automatically reloaded • jerry.email = 'jerry@snowgiraffe.com' • jerry.save! :on_duplicate_key_update => • [:password, :updated_at], • :reload => true, • :duplicate_columns => [:name]
More Customization • jerry.save(:keywords => 'LOW_PRIORITY', • :pre_sql => '/*Pre comment*/', • :post_sql => • "/*#{__FILE__} #{__LINE__}*/") • /*Pre comment*/ UPDATE LOW_PRIORITY `animals` • SET `created_at` = '2009-03-15 06:13:48', • `species_id` = NULL, `email` = NULL, • `password` = 'frenchfry', • `updated_at` = '2009-03-15 06:45:38', • `name` = 'Jerry G' • WHERE `id` = 7/*animal_controller.rb 147 */
Import (Bulk Insert) • Instead of one-by-one, insert a ton of records fast
Import (Bulk Insert) • Standard way: Insert each animal one by one • Animal.create!(:name => 'dolly dolphin', • :password => 'dolly') • Animal.create!(:name => 'big birdie', • :password => 'birdie') • and so on….
Fast Import: One INSERT • animals = [ Animal.new(:name => 'dolly dolphin', • :password => 'dolly'), • Animal.new(:name => 'big birdie', • :password => 'birdie')] • Animal.import animals • INSERT INTO `animals` • (`id`,`name`,`email`,`fav_beer`,`created_at`,`updated_at`,`password`) • VALUES • (NULL,'dolly dolphin',NULL,'Pabst', • '2009-03-20 00:17:15','2009-03-20 00:17:15','dolly'), • (NULL,'big birdie',NULL,'Pabst', • '2009-03-20 00:17:15','2009-03-20 00:17:15','birdie') • ON DUPLICATE KEY UPDATE `animals`.`updated_at`=VALUES(`updated_at`)
Fastest Import: fewer columns • columns = [ :name, :password ] • values = [['dolly dolphin', 'dolly'], • ['big birdie', 'birdie']] • options = {:validate => false, • :timestamps => false} • Animal.import columns, values, options • INSERT INTO `animals` (`name`,`password`) • VALUES • ('dolly dolphin','dolly'),('big birdie','birdie')
Insert Select • Standard: Query and Insert one by one • Species.find(:all).each do |s| • SpeciesZoo.create!(:species_id => s.id, • :zoo_id => zoo.id, • :extra_info => 'awesome') • end • Executes a query for each species • INSERT INTO `species_zoos` (`zoo_id`, `id`, `species_id`, `extra_info`) • VALUES (1, 3, 3, 'awesome') • INSERT INTO `species_zoos` (`zoo_id`, `id`, `species_id`, `extra_info`) • VALUES (1, 3, 2 , 'awesome') • And so on…
Insert Select Import • SpeciesZoo.insert_select( • :from => :species, • :select => ['species.id, ?', zoo], • :into => [:species_id, :zoo_id]) • One INSERT statement • INSERT INTO `species_zoos` • ( `species_id`, `zoo_id` ) • SELECT species.id, 1 FROM `species`
Not so good for slave replication Can be used as a sandbox then imported into a real table with ar-extensions gem Animal.create_temporary_table do |t| t.create!(:name => 'giraffe', :password => 'goo') Animal.insert_select( :from => t, :select => [:name, :password, :fav_beer], :into => [:name, :password, :fav_beer], :on_duplicate_key_update => [:password, :fav_beer]) end Temporary Tables
Customizing Find • Additional finder options • :keywords • :pre_sql • :post_sql • :index_hint
Customizing Find • Animal.find(:all, • :conditions => ['name = ?', 'Jerry G'], • :keywords => 'HIGH_PRIORITY', • :pre_sql => '/*Pre comment*/', • :post_sql => 'FOR UPDATE /*After the fact*/', • :index_hint => 'USE INDEX (uk_animal_name)' • ) • /*Pre comment*/ SELECT HIGH_PRIORITY * • FROM `animals` USE INDEX (uk_animal_name) • WHERE (name = 'Jerry G') FOR UPDATE • /*After the fact*/
Need more? Get dirty with find_by_sql • sql = Animal.send :finder_sql_to_string, • :conditions => ['name = ?', 'Jerry G'] • sql.gsub! /WHERE/, 'where /* Dirty hand */' • Animal.find_by_sql sql