现在的位置: 首页 > 综合 > 正文

rails active record validation and callbacks

2014年01月29日 ⁄ 综合 ⁄ 共 11576字 ⁄ 字号 评论关闭

Active Record uses the new_record? instance method to determine whether an object is already in the database or not.

The following methods trigger validations, and will save the object to the database only if the object is valid:

  • create
  • create!
  • save
  • save!
  • update
  • update_attributes
  • update_attributes!

The bang versions (e.g. save!) raise an exception if the record is invalid. The non-bang versions don’t:
save and update_attributes return false, create and
update just return the objects.

The following methods skip validations, and will save the object to the database regardless of its validity. They should be used with caution.

  • decrement!
  • decrement_counter
  • increment!
  • increment_counter
  • toggle!
  • update_all
  • update_attribute
  • update_counters

Note that save also has the ability to skip validations if passed
:validate => false
as argument. This technique should be used with caution.

  • save(:validate => false)

Note that an object instantiated with new will not report errors even if it’s technically invalid, because validations are not run when using
new.

Validation Helpers

validates_acceptance_of

This validation is very specific to web applications and this ‘acceptance’ does not need to be recorded anywhere in your database (if you don’t have a field for it, the helper will just create a virtual attribute).

validates_acceptance_of can receive an :accept option, which determines the value that will be considered acceptance. It defaults to “1”, but you can change this.

class Person < ActiveRecord::Base
  validates_acceptance_of :terms_of_service, :accept => 'yes'
end

validates_associated

class Library < ActiveRecord::Base
  has_many :books
  validates_associated :books
end

class Person < ActiveRecord::Base
  validates_confirmation_of :email
end

validates_confirmation_of

class Person < ActiveRecord::Base
  validates_confirmation_of :email
end

<%= text_field :person, :email %>
<%= text_field :person, :email_confirmation %>

validates_exclusion_of

class Account < ActiveRecord::Base
  validates_exclusion_of :subdomain, :in => %w(www),
    :message => "Subdomain %{value} is reserved."
end

This example uses the :message option to show how you can include the attribute’s value.

validates_format_of

class Product < ActiveRecord::Base
  validates_format_of :legacy_code, :with => /\A[a-zA-Z]+\z/,
    :message => "Only letters allowed"
end

validates_inclusion_of

class Coffee < ActiveRecord::Base
  validates_inclusion_of :size, :in => %w(small medium large),
    :message => "%{value} is not a valid size"
end

validates_length_of

  • :minimum – The attribute cannot have less than the specified length.
  • :maximum – The attribute cannot have more than the specified length.
  • :in (or :within) – The attribute length must be included in a given interval. The value for this option must be a range.
  • :is – The attribute length must be equal to the given value.
:wrong_length, :too_long, and :too_short
class Person < ActiveRecord::Base
  validates_length_of :bio, :maximum => 1000,
    :too_long => "%{count} characters is the maximum allowed"
end

validates_numericality_of

class Player < ActiveRecord::Base
  validates_numericality_of :points
  validates_numericality_of :games_played, :only_integer => true
end
    * :greater_than 
    * :greater_than_or_equal_to 
    * :equal_to 
    * :less_than
    * :less_than_or_equal_to 
    * :odd 
    * :even 

validates_presence_of

If you want to be sure that an association is present, you’ll need to test whether the foreign key used to map the association is present, and not the associated object itself.

class LineItem < ActiveRecord::Base
  belongs_to :order
  validates_presence_of :order_id
end

Since false.blank? is true, if you want to validate the presence of a boolean field you should use
validates_inclusion_of :field_name, :in => [true, false].

validates_uniqueness_of

class Holiday < ActiveRecord::Base
  validates_uniqueness_of :name, :scope => :year,
    :message => "should happen once per year"
end
class Person < ActiveRecord::Base
  validates_uniqueness_of :name, :case_sensitive => false
end

validates_with

class Person < ActiveRecord::Base
  validates_with GoodnessValidator
end
 
class GoodnessValidator < ActiveModel::Validator
  def validate
    if record.first_name == "Evil"
      record.errors[:base] << "This person is evil"
    end
  end
end

The validator class has two attributes by default:

  • record – the record to be validated
  • options – the extra options that were passed to validates_with
class Person < ActiveRecord::Base
  validates_with GoodnessValidator, :fields => [:first_name, :last_name]
end
 
class GoodnessValidator < ActiveRecord::Validator
  def validate
    if options[:fields].any?{|field| record.send(field) == "Evil" }
      record.errors[:base] << "This person is evil"
    end
  end
end

validates_each

It doesn’t have a predefined validation function. You should create one using a block, and every attribute passed to
validates_each will be tested against it. In the following example, we don’t want names and surnames to begin with lower case.

class Person < ActiveRecord::Base
  validates_each :name, :surname do |model, attr, value|
    model.errors.add(attr, 'must start with upper case') if value =~ /\A[a-z]/
  end
end

Common Validation Options

:allow_nil

class Coffee < ActiveRecord::Base
  validates_inclusion_of :size, :in => %w(small medium large),
    :message => "%{value} is not a valid size", :allow_nil => true
end

:allow_blank

:message

:on

The default behavior for all the built-in validation helpers is to be run on save (both when you’re creating a new record and when you’re updating it)

Conditional Validation

Using a Symbol with :if and
:unless

class Order < ActiveRecord::Base
  validates_presence_of :card_number, :if => :paid_with_card?
 
  def paid_with_card?
    payment_type == "card"
  end
end

Using a String with :if and
:unless

You can also use a string that will be evaluated using eval and needs to contain valid Ruby code.

class Person < ActiveRecord::Base
  validates_presence_of :surname, :if => "name.nil?"
end

Using a Proc with :if and :unless

class Account < ActiveRecord::Base
  validates_confirmation_of :password,
    :unless => Proc.new { |a| a.password.blank? }
end

Creating Custom Validation Methods

You must then register these methods by using one or more of the validate,
validate_on_create or validate_on_update class methods, passing in the symbols for the validation methods’ names.
You can pass more than one symbol for each class method and the respective validations will be run in the same order as they were registered.

class Invoice < ActiveRecord::Base
  validate :expiration_date_cannot_be_in_the_past,
    :discount_cannot_be_greater_than_total_value
 
  def expiration_date_cannot_be_in_the_past
    errors.add(:expiration_date, "can't be in the past") if
      !expiration_date.blank? and expiration_date < Date.today
  end
 
  def discount_cannot_be_greater_than_total_value
    errors.add(:discount, "can't be greater than total value") if
      discount > total_value
  end
end

You can even create your own validation helpers and reuse them in several different models. 

ActiveRecord::Base.class_eval do
  def self.validates_as_choice(attr_name, n, options={})
    validates_inclusion_of attr_name, {:in => 1..n}.merge(options)
  end
end

Simply reopen ActiveRecord::Base and define a class method like that. You’d typically put this code somewhere in
config/initializers.
You can use this helper like this:

class Movie < ActiveRecord::Base
  validates_as_choice :rating, 5
end

Working with Validation Errors

Of course, calling errors.clear upon an invalid object won’t actually make it valid: the
errors collection will now be empty, but the next time you call valid? or any method that tries to save this object to the database, the validations will run again. If any of the validations fail, the
errors collection will be filled again.

<%= form_for(@product) do |f| %>
  <%= f.error_messages %>
  <p>
    <%= f.label :description %><br />
    <%= f.text_field :description %>
  </p>
  <p>
    <%= f.label :value %><br />
    <%= f.text_field :value %>
  </p>
  <p>
    <%= f.submit "Create" %>
  </p>
<% end %>

<%= error_messages_for :product %>

<%= f.error_messages :header_message => "Invalid product!",
  :message => "You'll need to fix the following fields:",
  :header_tag => :h3 %>

  • .field_with_errors – Style for the form fields and labels with errors.
  • #errorExplanation – Style for the div element with the error messages.
  • #errorExplanation h2 – Style for the header of the div element.
  • #errorExplanation p – Style for the paragraph that holds the message that appears right below the header of the
    div element.
  • #errorExplanation ul li – Style for the list items with individual error messages.

Scaffolding for example generates public/stylesheets/scaffold.css, which defines the red-based style you saw above.

Customizing the Error Messages
HTML

The way form fields with errors are treated is defined by ActionView::Base.field_error_proc. This is a
Proc that receives two parameters:

  • A string with the HTML tag
  • An instance of ActionView::Helpers::InstanceTag.
ActionView::Base.field_error_proc = Proc.new do |html_tag, instance|
  if instance.error_message.kind_of?(Array)
    %(#{html_tag}<span class="validation-error"> 
      #{instance.error_message.join(',')}</span>).html_safe
  else
    %(#{html_tag}<span class="validation-error"> 
      #{instance.error_message}</span>).html_safe
  end
end

Callback Registration

class User < ActiveRecord::Base
  validates_presence_of :login, :email
 
  before_validation :ensure_login_has_a_value
 
  protected
  def ensure_login_has_a_value
    if login.nil?
      self.login = email unless email.blank?
    end
  end
end

class User < ActiveRecord::Base
  validates_presence_of :login, :email
 
  before_create {|user| user.name = user.login.capitalize
    if user.name.blank?}
end

10 Available Callbacks

10.1 Creating an Object

  • before_validation
  • after_validation
  • before_save
  • after_save
  • before_create
  • around_create
  • after_create

10.2 Updating an Object

  • before_validation
  • after_validation
  • before_save
  • after_save
  • before_update
  • around_update
  • after_update

10.3 Destroying an Object

  • before_destroy
  • after_destroy
  • around_destroy

after_initialize can be useful to avoid the need to directly override your Active Record
initialize method.

The after_find callback will be called whenever Active Record loads a record from the database.
after_find is called before after_initialize if both are defined.

They have no before_* counterparts, and the only way to register them is by defining them as regular methods. If you try to register
after_initialize or after_find using macro-style class methods, they will just be ignored. This behaviour is due to performance reasons, since
after_initialize and after_find will both be called for each record found in the database, significantly slowing down the queries.

class User < ActiveRecord::Base
  def after_initialize
    puts "You have initialized an object!"
  end
 
  def after_find
    puts "You have found an object!"
  end
end
 
>> User.new
You have initialized an object!
=> #<User id: nil>
 
>> User.first
You have found an object!
You have initialized an object!
=> #<User id: 1>

Running Callbacks

  • create
  • create!
  • decrement!
  • destroy
  • destroy_all
  • increment!
  • save
  • save!
  • save(false)
  • toggle!
  • update
  • update_attribute
  • update_attributes
  • update_attributes!
  • valid?

Additionally, the after_find callback is triggered by the following finder methods:

  • all
  • first
  • find
  • find_all_by_attribute
  • find_by_attribute
  • find_by_attribute!
  • last

The after_initialize callback is triggered every time a new object of the class is initialized.

Skipping Callbacks

  • decrement
  • decrement_counter
  • delete
  • delete_all
  • find_by_sql
  • increment
  • increment_counter
  • toggle
  • update_all
  • update_counters

class User < ActiveRecord::Base
  has_many :posts, :dependent => :destroy
end
 
class Post < ActiveRecord::Base
  after_destroy :log_destroy_action
 
  def log_destroy_action
    puts 'Post destroyed'
  end
end
 
>> user = User.first
=> #<User id: 1>
>> user.posts.create!
=> #<Post id: 1, user_id: 1>
>> user.destroy
Post destroyed
=> #<User id: 1>

Conditional Callbacks

the same to validation.

Callback Classes

class PictureFileCallbacks
  def after_destroy(picture_file)
    File.delete(picture_file.filepath)
      if File.exists?(picture_file.filepath)
  end
end
class PictureFile < ActiveRecord::Base
  after_destroy PictureFileCallbacks.new
end

class PictureFileCallbacks
  def self.after_destroy(picture_file)
    File.delete(picture_file.filepath)
      if File.exists?(picture_file.filepath)
  end
end

class PictureFile < ActiveRecord::Base
  after_destroy PictureFileCallbacks
end

You can declare as many callbacks as you want inside your callback classes.

Observers

rails generate observer User

class UserObserver < ActiveRecord::Observer
  def after_create(model)
    # code to send confirmation email...
  end
end

Observers are conventionally placed inside of your app/models directory and registered in your application’s
config/application.rb
file. For example, the UserObserver above would be saved as
app/models/user_observer.rb and registered in config/application.rb this way:

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

As usual, settings in config/environments take precedence over those in
config/application.rb. So, if you prefer that an observer doesn’t run in all environments, you can simply register it in a specific environment instead.


sharing observers

class MailerObserver < ActiveRecord::Observer
  observe :registration, :user
 
  def after_create(model)
    # code to send confirmation email...
  end
end

抱歉!评论已关闭.