Active Record Basics

Fundamentals

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

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