Rails 3 Model: Difference between revisions

From Wiki
Jump to navigation Jump to search
 
(18 intermediate revisions by the same user not shown)
Line 155: Line 155:
has_many :articles, :dependent => :destroy        # destroy article when user is destroyed, also ":delete" and ":nullify"
has_many :articles, :dependent => :destroy        # destroy article when user is destroyed, also ":delete" and ":nullify"
</source>
</source>
==== has_many :through ====
<source lang="ruby">
class User < ActiveRecord::Base
    has_many :articles, :order => 'published_at DESC, title ASC', :dependent => :nullify
    has_many :replies, :through => :articles, :source => :comments
end
</source>
<code>user.articles.comments</code> becomes shortened to <code>user.replies</code>


=== Many-to-many ===
=== Many-to-many ===
Line 176: Line 185:
     has_and_belongs_to_many :articles  
     has_and_belongs_to_many :articles  
end
end
</source>
To add/drop an association:
<source lang="ruby">
user.articles.push(category)
user.articles.delete(category)
</source>
</source>


Line 206: Line 221:
Article.last                  # same as Article.find(:last)
Article.last                  # same as Article.find(:last)
Article.all                    # same as Article.find(:all)
Article.all                    # same as Article.find(:all)
Article.where(:title => 'RailsConf').first  # chaining
Article.where(:title => 'RailsConf').first   # condition and chaining
Article.find_by_title('RailsConf') # dynamic finder
Article.where("published_at < ?", Time.now)  # SQL fragment with safe variable substitution
Article.where("title LIKE :search OR body LIKE :search", {:search => '%association%'}) # hash for variable substitution
Article.find_by_title('RailsConf')           # dynamic finder
User.first.articles.all        # more chaining
Article.order("published_at DESC")
Article.limit(1)
Article.joins(:comments)      # join the other table
Article.includes(:comments)    # join the other table and load associated Active Record objects
</source>
 
=== Scope ===
==== Default scope ====
<source lang="ruby">
class Category < ActiveRecord::Base
    has_and_belongs_to_many :articles 
    default_scope order('categories.name')
end
</source>
Specifies default sort order of query results.
 
==== Named scope ====
<source lang="ruby">
class Article < ActiveRecord::Base
    validates :title, :presence => true
    validates :body, :presence => true
    belongs_to :user has_and_belongs_to_many :categories
    has_many :comments
    scope :published, where("articles.published_at IS NOT NULL")
    scope :draft, where("articles.published_at IS NULL")
    scope :recent, lambda { published.where("articles.published_at > ?", 1.week.ago.to_date)}
    scope :where_title, lambda { |term| where("articles.title LIKE ?", "%#{term}%") }
end
</source>
Defines named searches.  To use these:
<source lang="ruby">
Article.recent
Article.draft.where_title("one")
</source>
</source>


Line 230: Line 281:
</source>
</source>


== Validation and Errors ==
== Validation ==
<source lang="ruby">
<source lang="ruby">
class Article< ActiveRecord::Base
class Article< ActiveRecord::Base
Line 245: Line 296:
article.errors.on(:title)    # "can't be blank"
article.errors.on(:title)    # "can't be blank"
article.valid?                # false
article.valid?                # false
</source>
Other validators
<source lang="ruby">
class Account < ActiveRecord::Base
    validates :login, :presence => true
    validates :password, :confirmation => true    # creates password_confirmation method
    validates :email, :uniqueness => true,
                      :length => { :within => 5..50 },
                      :format => { :with => /^[^@][\w.-]+@[\w.-]+[.][a-z]{2,4}$/i }
    validates :terms_of_service, :acceptance => true  # terms_of_service is boolean
end
</source>
=== Length validation options ===
{|border=1 cellpadding=5
|Option
|Description
|----
|:minimum
|Specifies the minimum size of the attribute
|----
|:maximum
|Specifies the maximum size of the attribute
|----
|:is
|Specifies the exact size of the attribute
|----
|:within
|Specifies the valid range (as a Ruby Range object) of values acceptable for the attribute
|----
|:allow_nil
|Specifies that the attribute may be nil; if so, the validation is skipped
|----
|:too_long
|Specifies the error message to add if the attribute exceeds the maximum
|----
|:too_short
|Specifies the error message to add if the attribute is below the minimum
|----
|:wrong_length
|Specifies the error message to add if the attribute is of the wrong size
|----
|:message
|Specifies the error message to add if :minimum, :maximum, or :is is violated
|----
|}
=== Custom Validations ===
<source lang="ruby">
class Article < ActiveRecord::Base
    ...
    def published?
        published_at.present?
    end
end
</source>
<source lang="ruby">
class Comment < ActiveRecord::Base
    belongs_to :article
    validates :name, :email, :body, :presence => true
    validate :article_should_be_published            # not "validates"
    def article_should_be_published
        errors.add(:article_id, "is not published yet") if article && !article.published?
    end
end
</source>
See error message with <code>comment.errors.full_messages</code>
== Callbacks and Observers ==
=== Callback ===
A callback is stored in the model:
<source lang="ruby">
class Comment < ActiveRecord::Base
    after_create :email_article_author
    def email_article_author
        puts "We will notify #{article.user.email} in Chapter 9"
    end
end
</source>
Other callbacks:
<source lang="ruby">
before_create, after_create, before_save, after_save, before_destroy, after_destroy
</source>
=== Observer ===
An observer is stored outside the model to avoid clutter:
<pre>
rails generate observer Comment
</pre>
creates <code>file app/models/comment_observer.rb</code>:
<source lang="ruby">
class CommentObserver < ActiveRecord::Observer
    def after_create(comment)
        puts " We will notify the author in Chapter 9"
    end
end
</source>
Need to include this line in <code>config/application.rb</code>:
<source lang="ruby">
# Activate observers that should always be running
config.active_record.observers = :comment_observer
</source>
== User Authentication Example ==
<source lang="ruby">
require 'digest'
class User < ActiveRecord::Base
    attr_accessor :password  # actual db field is "hashed_password"
    validates :email,
        :uniqueness => true,
        :length => { :within => 5..50 },
        :format => { :with => /^[^@][\w.-]+@[\w.-]+[.][a-z]{2,4}$/i }
    validates :password,
        :confirmation => true,
        :length => { :within => 4..20 },
        :presence => true,
        :if => :password_required? 
    has_one :profile has_many :articles, :order => 'published_at DESC, title ASC', :dependent => :nullify
    has_many :replies, :through => :articles, :source => :comments 
    before_save :encrypt_new_password 
    def self.authenticate(email, password)
        user = find_by_email(email)
        return user if user && user.authenticated?(password)
    end 
    def authenticated?(password)
        self.hashed_password == encrypt(password)
    end 
    protected
    def encrypt_new_password
        return if password.blank?
        self.hashed_password = encrypt(password)
    end 
    def password_required?
        hashed_password.blank? || password.present?
    end 
    def encrypt(string)
        Digest::SHA1.hexdigest(string)
    end
end
</source>
</source>

Latest revision as of 21:40, 8 September 2011

Model Definition

  • Generate a model to create model objects and underlying tables:
rails generate model Category name:string

This creates both a model definition file in app/models and a database migration file in db/migrate

  • Generate a migration when you just want to modify the database:
rails generate migration create_articles_categories  # create join table

This makes a new database migration script in db/migrate. Edit this as necessary before running the migration.

  • To run all outstanding migrations:
rake db:migrate
  • To roll back to a particular timestamp:
rake db:migrate VERSION=20090124223305

(see the schema_migrations table in your database for a definitive list of timestamps)


Migrations

Create table options

  • column definitions
t.column(:name, :string)  # verbose style
t.string :name            # shorter style
  • column types:
binary (aka blob), boolean, date, datetime, decimal, float, integer, string, text, time, timestamp
decimal has :precision (total number of digits) and :scale (number of digits after decimal place)
  • column options:
:default => value
:limit => size
:null => false
  • change column definition:
t.change(:name, :string, :limit => 80)
  • rename column:
t.rename(:description, :name)
  • foreign key:
create_table :accounts do 
    t.belongs_to(:person) 
end
  • add Active Record-maintained timestamp (created_at and updated_at) columns to the table.
t.timestamps
  • make an index:
t.index(:name)                                     # a simple index 
t.index([:branch_id, :party_id], :unique => true)  # a unique index
  • To create a join table with no default id primary key:
create_table :ingredients_recipes, :id => false do |t|
    t.column :ingredient_id, :integer
    t.column :recipe_id, :integer 
end

Seed Data

A default part of the rails app is the file db/seeds.rb. Use this to seed the database with test data:

user = User.create :email => 'mary@example.com', :password => 'guessit' 
Category.create [{:name => 'Programming'}, {:name => 'Event'}, {:name => 'Travel'}, {:name => 'Music'}, {:name => 'TV'}]

Then load the seed data:

rake db:seed    # just populate with seed data, can introduce duplicates
rake db:setup   # recreates the database and populates with seed data

Associations

One-to-one

class User < ActiveRecord::Base
    has_one :profile           # creates User.profile method
end                            
-------------------------------
class Profile < ActiveRecord::Base 
    belongs_to :user           # assumes Profile.user_id column
end                            # creates Profile.user method

REMINDER: the belongs_to declaration always goes in the class with the foreign key

has_one automatic methods

user.profile #=> #<Profile id: 2, user_id: 1, ...> 
user.profile.nil? #=> false 
user.build_profile(:bio => 'eats leaves') #=> #<Profile id: nil, user_id: 1, ...>   # not automatically saved
user.create_profile(:bio => 'eats leaves') #=> #<Profile id: 3, user_id: 1, ...>    # automatically saved

has_one options

has_one :profile, :class_name => 'Account'      # refer to user.account instead of user.profile
has_one :profile, :foreign_key => 'account_id'  # refer to user.account instead of user.profile
has_one :profile, :conditions => "active = 1"   # only specific profiles are considered
has_one :profile, :dependent => :destroy        # call destroy on profile when user is destroyed, also ":delete" and ":nullify"

One-to-many

class User < ActiveRecord::Base
    has_many :articles           # creates User.articles method
end
-------------------------------
class Article < ActiveRecord::Base
    belongs_to :user             # assumes Article.user_id column
end                              # creates Article.user method

has_many automatic methods

user.articles.size               # array length
user.article_ids                 # list of ids
user.articles << Article.first   # automatically saves association in Article.user_id
user.articles.delete(articles)   # remove articles by setting Article.user_id to null
user.articles.clear              # remove all articles by setting Article.user_id to null
user.articles.find(conditions)   # find a subset of user.articles
user.articles.build(:title => 'Ruby 1.9')   # return associated article but don't save yet
user.articles.create(:title => 'Ruby 1.9')  # return associated article that has already been saved

has_many options

has_many :articles, :class_name => 'Post'          # refer to user.posts instead of user.articles
has_many :articles, :foreign_key => 'post_id'      # refer to user.posts instead of user.articles
has_many :articles, :conditions => "active = 1"    # only consider specific articles
has_many :articles, :order => "published_at DESC"  # default order of user.articles
has_many :articles, :dependent => :destroy         # destroy article when user is destroyed, also ":delete" and ":nullify"

has_many :through

class User < ActiveRecord::Base
    has_many :articles, :order => 'published_at DESC, title ASC', :dependent => :nullify 
    has_many :replies, :through => :articles, :source => :comments 
end

user.articles.comments becomes shortened to user.replies

Many-to-many

This requires a join table:

rails generate migration create_articles_categories

simple join

class Article < ActiveRecord::Base 
    validates :title, :presence => true 
    validates :body, :presence => true  
    belongs_to :user 
    has_and_belongs_to_many :categories
end
class Category < ActiveRecord::Base 
    has_and_belongs_to_many :articles 
end

To add/drop an association:

user.articles.push(category)
user.articles.delete(category)

rich join

Create a model in addition to the join table. This allows the join itself to have other properties.

Model Manipulation

Create and Save

article = Article.new  # make an empty object
article.title = "My Title"  # add attributes
article.author = "Herman"
article.save  # save to database
article.create(:title => "My Title", :author => "Herman")  # make, set attributes, and save
article.new_record?  # new means not saved to database

Find

Article.find(3)                # look for id = 3
Article.first                  # same as Article.find(:first), uses "LIMIT 1" in SQL, so may not be id = 1
Article.last                   # same as Article.find(:last)
Article.all                    # same as Article.find(:all)
Article.where(:title => 'RailsConf').first   # condition and chaining
Article.where("published_at < ?", Time.now)  # SQL fragment with safe variable substitution
Article.where("title LIKE :search OR body LIKE :search", {:search => '%association%'})  # hash for variable substitution
Article.find_by_title('RailsConf')           # dynamic finder
User.first.articles.all        # more chaining
Article.order("published_at DESC")
Article.limit(1)
Article.joins(:comments)       # join the other table
Article.includes(:comments)    # join the other table and load associated Active Record objects

Scope

Default scope

class Category < ActiveRecord::Base 
    has_and_belongs_to_many :articles  
    default_scope order('categories.name') 
end

Specifies default sort order of query results.

Named scope

class Article < ActiveRecord::Base
    validates :title, :presence => true
    validates :body, :presence => true
    belongs_to :user has_and_belongs_to_many :categories
    has_many :comments
    scope :published, where("articles.published_at IS NOT NULL")
    scope :draft, where("articles.published_at IS NULL")
    scope :recent, lambda { published.where("articles.published_at > ?", 1.week.ago.to_date)}
    scope :where_title, lambda { |term| where("articles.title LIKE ?", "%#{term}%") }
end

Defines named searches. To use these:

Article.recent
Article.draft.where_title("one")

Update

article = Article.first
article.title = "Rails 3 is great"
article.published_at = Time.now
article.save
article = Article.first
article.update_attributes(:title => "RailsConf2010", :published_at => 1.day.ago)  # saves too

Delete

Article.find(3).destroy  # find, then destroy
Article.destroy(3)       # same thing
Article.delete(3)        # delete directly from database with no object callbacks
Article.delete_all("published_at < '2011-01-01'")  # conditional

Validation

class Article< ActiveRecord::Base
    validates :title, :presence => true
    validates :body, :presence => true
end
article = Article.new
article.errors.any?           # false
article.save                  # returns false because of validation
article.errors.full_messages  # ["Title can't be blank", "Body can't be blank"]
article.errors.on(:title)     # "can't be blank"
article.valid?                # false

Other validators

class Account < ActiveRecord::Base 
    validates :login, :presence => true 
    validates :password, :confirmation => true    # creates password_confirmation method
    validates :email, :uniqueness => true, 
                      :length => { :within => 5..50 }, 
                      :format => { :with => /^[^@][\w.-]+@[\w.-]+[.][a-z]{2,4}$/i }
    validates :terms_of_service, :acceptance => true   # terms_of_service is boolean
end

Length validation options

Option Description
:minimum Specifies the minimum size of the attribute
:maximum Specifies the maximum size of the attribute
:is Specifies the exact size of the attribute
:within Specifies the valid range (as a Ruby Range object) of values acceptable for the attribute
:allow_nil Specifies that the attribute may be nil; if so, the validation is skipped
:too_long Specifies the error message to add if the attribute exceeds the maximum
:too_short Specifies the error message to add if the attribute is below the minimum
:wrong_length Specifies the error message to add if the attribute is of the wrong size
:message Specifies the error message to add if :minimum, :maximum, or :is is violated

Custom Validations

class Article < ActiveRecord::Base
    ...
    def published? 
        published_at.present? 
    end
end
class Comment < ActiveRecord::Base 
    belongs_to :article
    validates :name, :email, :body, :presence => true 
    validate :article_should_be_published             # not "validates"
    def article_should_be_published 
        errors.add(:article_id, "is not published yet") if article && !article.published? 
    end 
end

See error message with comment.errors.full_messages

Callbacks and Observers

Callback

A callback is stored in the model:

class Comment < ActiveRecord::Base
    after_create :email_article_author
    def email_article_author
        puts "We will notify #{article.user.email} in Chapter 9" 
    end 
end

Other callbacks:

before_create, after_create, before_save, after_save, before_destroy, after_destroy

Observer

An observer is stored outside the model to avoid clutter:

rails generate observer Comment

creates file app/models/comment_observer.rb:

class CommentObserver < ActiveRecord::Observer 
    def after_create(comment) 
        puts " We will notify the author in Chapter 9"
    end 
end

Need to include this line in config/application.rb:

# Activate observers that should always be running 
config.active_record.observers = :comment_observer

User Authentication Example

require 'digest' 
class User < ActiveRecord::Base 
    attr_accessor :password  # actual db field is "hashed_password"
    validates :email, 
        :uniqueness => true, 
        :length => { :within => 5..50 }, 
        :format => { :with => /^[^@][\w.-]+@[\w.-]+[.][a-z]{2,4}$/i } 
    validates :password, 
        :confirmation => true, 
        :length => { :within => 4..20 }, 
        :presence => true, 
        :if => :password_required?  
    has_one :profile has_many :articles, :order => 'published_at DESC, title ASC', :dependent => :nullify 
    has_many :replies, :through => :articles, :source => :comments  
    before_save :encrypt_new_password  
    def self.authenticate(email, password) 
        user = find_by_email(email) 
        return user if user && user.authenticated?(password) 
    end  
    def authenticated?(password) 
        self.hashed_password == encrypt(password) 
    end  

    protected 
    def encrypt_new_password 
        return if password.blank? 
        self.hashed_password = encrypt(password) 
    end  
    def password_required? 
        hashed_password.blank? || password.present? 
    end  
    def encrypt(string) 
        Digest::SHA1.hexdigest(string) 
    end 
end