Active Record Basics

Fundamentals

  • One database table maps to one Ruby class. The Ruby class is called a model.
  • Ruby classes live under app/models and extend ActiveRecord::Base
  • Table names are plural and class names are singular
  • Database columns map to attributes, i.e. get and set methods, in the model class
  • All tables have an integer primary key, by convention called id
  • Database tables are created with migrations

Overriding Naming Conventions

class MyModel < ActiveRecord::Base
 self.table_name = 'my_legacy_table'
 self.primary_key = 'my_id'
 self.pluralize_table_names = false
 self.table_name_prefix = 'my_app'
end

CRUD

Operation Method
Create create, new, save
Read find, find_by_<attr>
Update save, update_attributes
Delete destroy

create = new + save

user = User.new
user.first_name = "Dave" 
user.last_name = "Thomas" 
user.save
<=>
user = User.new(
 :first_name => "Dave",
 :last_name => "Thomas" 
)
user.save
user.create(
 :first_name => "Dave",
 :last_name => "Thomas" 
)

save!

user = User.new(
 :first_name => "Dave",
 :last_name => "Thomas" 
)
if user.save
 # All is ok
else
 # Could not save user
end
begin
 user.save!
rescue ActiveRecord::RecordInvalid => e
 # Could not save user
end

Column/Attribute Data Types

MySQL Ruby Class
integer Fixnum
clob, blob, text String
float, double Float
char, varchar String
datetime, time Time

Virtual Attributes

 # Virtual attributes are attributes that do not correspond
 # directly to database columns.
 class Song < ActiveRecord::Base
 def length=(minutes)
 # self[:length] = minutes*60
 write_attribute(:length, minutes * 60)
 end
 
 def length
 # self[:length] / 60
 read_attribute(:length) / 60
 end
end

Default Attribute Values

class User < ActiveRecord::Base
 def language
 read_attribute(:language) || "sv" 
 end
end

Attribute Query Methods

user = User.new(:name => "David")
# user.name? is equivalent to user.name.present?
# If it returns true then the name is not nil and not empty
user.name? # => true
anonymous = User.new(:name => "")
anonymous.name? # => false

Boolean Attributes

  • Everything except nil and false is true in Ruby
  • In MySQL boolean columns are char(1) with values 0 or 1, both of which are true in Ruby.
  • Instead of using user.admin use user.admin?
  • When you add the question mark, false is returned for all of the following: the number 0, one of the strings ‘0’, ‘f’, ‘false’, ’’, or the constant false

find

 User.find(:first) # => First user object
 User.first # short hand for find(:first)
 User.find(:last) # => Last user object
 User.last # short hand for find(:last)
 User.find(:all) # => Array with all User objects
 User.all # short hand for find(:all)
 User.find(3) # => User object with id 3

find with :conditions

User.find(:all, 
 :conditions =>
 ["first_name = ? and created_at > ?", "David", 1.year.ago])
User.find(:all, 
 :conditions => 
 ["first_name = :first_name, last_name = :last_name",
 {:first_name => "David", :last_name => "Letterman"}])
 
User.find(:all, 
 :conditions => {:first_name => "Jamis", :last_name => "Buck"})
User.find(:all, :conditions => [ "category IN (?)", categories])

Merging Conditions

Post.merge_conditions(
 {:title => 'Lucky Star'},
 ['rating IN (?)', 1..5]
)
=> "(`posts`.`title` = 'Lucky Star') AND (rating IN (1,2,3,4,5))" 

Everything is a find :all

# select * from users limit 1
User.find(:first) <=> User.find(:all, :limit => 1).first
# select * from users where id = 1
User.find(1) <=> User.find(:all, :conditions => "users.id = 1").first

Like Clauses

# This works
User.find(:all,
 :conditions => ["name like ?", "%" + params[:name] + "%")
# This doesn't work
User.find(:all,
 :conditions => ["name like '%?%'", params[:name])

Dynamic Finders

User.find_by_first_name "Peter" 
User.find_all_by_last_name "Hanson" 
User.find_by_age "20" 
User.find_by_last_name("Buck",
 :conditions => {:country = "Sweden", :age => 20..30})
User.find_by_first_name_and_last_name "Andreas", "Kviby" 

Named Scopes

class Article < ActiveRecord::Base
 named_scope :approved, :conditions => {:status => 'approved'}
 named_scope :since, lambda { |time_ago| { :conditions => ['published_at > ?', time_ago] }}
 named_scope :inactive, :conditions => {:active => false} do
 def activate
 each { |article| article.update_attribute(:active, true) }
 end
 end
end
Article.approved.since(3.days.ago) # all approved articles in the last three days
Article.inactive.activate # activate all inactive articles

RecordNotFound Exception

User.exists?(999) # => false
User.find(999) # => raises ActiveRecord::RecordNotFound
User.find_by_id(999) # => nil
User.find(:first, :conditions => {:id => 999}) # => nil

Find or Create

# No 'Summer' tag exists
Tag.find_or_create_by_name("Summer") # equal to Tag.create(:name => "Summer")
 
# Now the 'Summer' tag does exist
Tag.find_or_create_by_name("Summer") # equal to Tag.find_by_name("Summer")
# No 'Winter' tag exists
winter = Tag.find_or_initialize_by_name("Winter")
winter.new_record? # true
winter.save

Update

order = Order.find(12)
order.name = "Bill Gates" 
order.charge = 10000
order.save!
<=>
order = Order.find(13)
order.update_attributes!(
 :name => "Bill Gates",
 :charge => 10000
)

update_attributes is Syntactic Sugar

def update_attributes(attributes)
 self.attributes = attributes
 save
end
 
def update_attributes!(attributes)
 self.attributes = attributes
 save!
end

Partial Updates

user = User.find_by_name("Dave" )
user.changed? # => false
user.name = "Dave Thomas" 
user.changed? # => true
user.changed # => ['name']
user.changes # => {"name"=>["dave", "Dave Thomas"]}
user.name_changed? # => true
user.name_was # => 'Dave'
user.name_change # => ['Dave', 'Dave Thomas']
user.name = 'Bill'
user.name_change # => ['Dave', 'Dave Thomas']
user.save # updates only the name in the db
user.changed? # => false

Locking

# SELECT * FROM accounts WHERE (account.`id` = 1) FOR UPDATE
account = Account.find(id, :lock => true)
account.status = 'disabled'
account.save!
# Optimistic locking with integer column lock_version in the accounts table:
account1 = Account.find(4)
account2 = Account.find(4)
account1.update_attributes(:status => 'disabled')
account2.update_attributes(:status => 'enabled') # => Raises ActiveRecord::StaleObjectError

destroy

# Instance method User#destroy
User.count # => 5
u = User.find(:first)
u.destroy
User.count # => 4
# Class method User.destroy
User.count # => 4
User.destroy(2, 3)
User.count # => 2
User.exists?(2) # => false
# Class method User.destroy_all
User.destroy_all("id >= 5")
User.count # => 1
User.destroy_all
User.count # => 0

destroy Class Methods

def destroy(id)
 if id.is_a?(Array)
 id.map { |one_id| destroy(one_id) }
 else
 find(id).destroy
 end
end
def destroy_all(conditions = nil)
 find(:all, :conditions => conditions).each do |object|
 object.destroy
 end
end

delete: Does not Instantiate Objects

# Post.delete_all("person_id = 5 AND (category = 'Something' OR category = 'Else')")
def delete_all(conditions = nil)
 sql = "DELETE FROM #{quoted_table_name} " 
 add_conditions!(sql, conditions, scope(:find))
 connection.delete(sql, "#{name} Delete all")
end
# Todo.delete(1)
# Todo.delete([2,3,4])
def delete(id)
 delete_all([ "#{connection.quote_column_name(primary_key)} IN (?)", id ])
end

Calculations

Person.minimum('age')
Person.maximum('age')
Person.sum('age')
Person.count(:conditions => ["age > ?", 25])
Person.average('age')
Person.calculate(:std, :age)

Executing SQL

# Post.find_by_sql ["SELECT title FROM posts created_at > ?", start_date]
def find_by_sql(sql)
 connection.select_all(sanitize_sql(sql), "#{name} Load").collect! { |record| instantiate(record) }
end
ActiveRecord::Base.connection.execute("delete from users")
ActiveRecord::Base.connection.select_one("select * from users where id = 1")
ActiveRecord::Base.connection.select_all("select * from users")
ActiveRecord::Base.connection.select_value("select name from users where id = 1")
ActiveRecord::Base.connection.select_values("select name from users")

Serializing Attributes

class Person < ActiveRecord::Base
 serialize params
end
person = Person.new
person.params = {
 :height => 190,
 :weight => 80,
 :eye_color => 'blue'
}
person.save # Serializes the hash in YAML format in the db

Aliased Attributes

class Aricle < ActiveRecord::Base
 # Now the body attribute can be accessed with a text method too
 alias_attribute :text, :body
end
a = Article.first
a.body # => "the body" 
a.text # => "the body" 
a.text? # => true
a.text = "new body" 
a.body # => "new body" 

Delegating Attributes

class User < ActiveRecord::Base
 delegate :street, :city, :to => :address
end
u = User.first
u.address.street # => "Fifth Avenue" 
u.street # => "Fifth Avenue" 

Caching Attributes

class Person < ActiveRecord::Base
 def social_security
 decrypt_social_security
 end
 # Memoize the result of the social_security method after its first evaluation.
 # Must be placed after the target method definition.
 memoize :social_security
end
@person = Person.new
@person.social_security # decrypt_social_security is invoked
@person.social_security # decrypt_social_security is NOT invoked

Defining Composite Attributes

class Name 
 attr_reader :first, :initials, :last 
 def initialize(first, initials, last) 
 @first = first 
 @initials = initials 
 @last = last 
 end 
 def to_s 
 [ @first, @initials, @last ].compact.join(" ") 
 end 
end 
class Customer < ActiveRecord::Base 
 composed_of :name, 
 :class_name => Name, 
 :mapping => 
 [#database ruby
 [:first_name, :first], 
 [:initials, :initials], 
 [:last_name, :last] 
end 

Using Composite Attributes

name = Name.new("Dwight" , "D" , "Eisenhower" )
Customer.create(:credit_limit => 1000, :name => name)
customer = Customer.find(:first)
puts customer.name.first #=> Dwight
puts customer.name.last #=> Eisenhower
puts customer.name.to_s #=> Dwight D Eisenhower
customer.name = Name.new("Harry" , nil, "Truman" )
customer.save

Transactions

Account.transaction do
 account1.deposit(100)
 account2.withdraw(100)
end

The Illusion of Simplicity

“ActiveRecord is an example of a leaky abstraction and you need to understand the SQL that it generates to avoid gotchas such as the N+1 problem.”

– Chad Fowler

AltStyle によって変換されたページ (->オリジナル) /