Rails 3 Model: Difference between revisions
(44 intermediate revisions by the same user not shown) | |||
Line 1: | Line 1: | ||
= Model Definition = | |||
* Generate a model to create model objects and underlying tables: | |||
<pre> | |||
rails generate model Category name:string | |||
</pre> | |||
This creates both a model definition file in <code>app/models</code> and a database migration file in <code>db/migrate</code> | |||
* Generate a migration when you just want to modify the database: | |||
<pre> | |||
rails generate migration create_articles_categories # create join table | |||
</pre> | |||
This makes a new database migration script in <code>db/migrate</code>. Edit this as necessary before running the migration. | |||
* To run all outstanding migrations: | |||
<pre> | |||
rake db:migrate | |||
</pre> | |||
* To roll back to a particular timestamp: | |||
<pre> | |||
rake db:migrate VERSION=20090124223305 | |||
</pre> | |||
(see the <code>schema_migrations</code> table in your database for a definitive list of timestamps) | |||
== Migrations == | |||
=== Create table options === | |||
* column definitions | |||
<source lang="ruby"> | |||
t.column(:name, :string) # verbose style | |||
t.string :name # shorter style | |||
</source> | |||
* column types: | |||
<pre> | |||
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) | |||
</pre> | |||
* column options: | |||
<source lang="ruby"> | |||
:default => value | |||
:limit => size | |||
:null => false | |||
</source> | |||
* change column definition: | |||
<source lang="ruby"> | |||
t.change(:name, :string, :limit => 80) | |||
</source> | |||
* rename column: | |||
<source lang="ruby"> | |||
t.rename(:description, :name) | |||
</source> | |||
* foreign key: | |||
<source lang="ruby"> | |||
create_table :accounts do | |||
t.belongs_to(:person) | |||
end | |||
</source> | |||
* add Active Record-maintained timestamp (<code>created_at</code> and <code>updated_at</code>) columns to the table. | |||
<source lang="ruby"> | |||
t.timestamps | |||
</source> | |||
* make an index: | |||
<source lang="ruby"> | |||
t.index(:name) # a simple index | |||
t.index([:branch_id, :party_id], :unique => true) # a unique index | |||
</source> | |||
* To create a join table with no default <code>id</code> primary key: | |||
<source lang="ruby"> | |||
create_table :ingredients_recipes, :id => false do |t| | |||
t.column :ingredient_id, :integer | |||
t.column :recipe_id, :integer | |||
end | |||
</source> | |||
=== Seed Data === | |||
A default part of the rails app is the file <code>db/seeds.rb</code>. Use this to seed the database with test data: | |||
<source lang="ruby"> | |||
user = User.create :email => 'mary@example.com', :password => 'guessit' | |||
Category.create [{:name => 'Programming'}, {:name => 'Event'}, {:name => 'Travel'}, {:name => 'Music'}, {:name => 'TV'}] | |||
</source> | |||
Then load the seed data: | |||
<pre> | |||
rake db:seed # just populate with seed data, can introduce duplicates | |||
rake db:setup # recreates the database and populates with seed data | |||
</pre> | |||
== Associations == | |||
=== One-to-one === | |||
<source lang="ruby"> | |||
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 | |||
</source> | |||
REMINDER: the <code>belongs_to</code> declaration always goes in the class with the foreign key | |||
==== has_one automatic methods ==== | |||
<source lang="ruby"> | |||
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 | |||
</source> | |||
==== has_one options ==== | |||
<source lang="ruby"> | |||
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" | |||
</source> | |||
=== One-to-many === | |||
<source lang="ruby"> | |||
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 | |||
</source> | |||
==== has_many automatic methods ==== | |||
<source lang="ruby"> | |||
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 | |||
</source> | |||
==== has_many options ==== | |||
<source lang="ruby"> | |||
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" | |||
</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 === | |||
This requires a join table: | |||
<pre> | |||
rails generate migration create_articles_categories | |||
</pre> | |||
==== simple join ==== | |||
<source lang="ruby"> | |||
class Article < ActiveRecord::Base | |||
validates :title, :presence => true | |||
validates :body, :presence => true | |||
belongs_to :user | |||
has_and_belongs_to_many :categories | |||
end | |||
</source> | |||
<source lang="ruby"> | |||
class Category < ActiveRecord::Base | |||
has_and_belongs_to_many :articles | |||
end | |||
</source> | |||
To add/drop an association: | |||
<source lang="ruby"> | |||
user.articles.push(category) | |||
user.articles.delete(category) | |||
</source> | |||
==== 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 == | == Create and Save == | ||
Line 23: | 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 # | Article.where(:title => 'RailsConf').first # condition and chaining | ||
Article.find_by_title('RailsConf') | 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 47: | Line 281: | ||
</source> | </source> | ||
== Validation | == Validation == | ||
<source lang="ruby"> | <source lang="ruby"> | ||
class Article< ActiveRecord::Base | class Article< ActiveRecord::Base | ||
Line 64: | Line 298: | ||
</source> | </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"> | <source lang="ruby"> | ||
before_create, after_create, before_save, after_save, before_destroy, after_destroy | |||
</source> | </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"> | <source lang="ruby"> | ||
class CommentObserver < ActiveRecord::Observer | |||
def after_create(comment) | |||
puts " We will notify the author in Chapter 9" | |||
end | |||
end | |||
</source> | </source> | ||
Need to include this line in <code>config/application.rb</code>: | |||
<source lang="ruby"> | <source lang="ruby"> | ||
# Activate observers that should always be running | |||
config.active_record.observers = :comment_observer | |||
</source> | </source> | ||
== | == User Authentication Example == | ||
<source lang="ruby"> | <source lang="ruby"> | ||
class | require 'digest' | ||
has_many : | 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 | 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
andupdated_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