Ruby on Rails 3 Model Data Validation

Rails Model Data Validation

In app/models/account.rb, add declarative Rails data validation

class Account < ActiveRecord::Base
  validates_presence_of :user_name          # Declare "user_name" is a required field
end
  • validates_presence_of indicates a model attribute is mandatory

Provide a custom error message if validation failed

class Account < ActiveRecord::Base
  validates_presence_of :user_name, :message => "must be provided"
end

Compose multiple Rails validation rules

class Account < ActiveRecord::Base
  validates_presence_of :user_name, :message => " is mandatory"

  validates_length_of :user_name, :in => 6..12 , :message => " must be 6 to 12 characters"

  validates_format_of :user_name, :with => /\w+/, :message => " can only contain characters"
  validates_format_of :user_name, :with => /[A-Z]/, :message => " must have one upper case"
end

Rails Validation Rules

Validate Mandatory Model Attribute(s)

  validates_presence_of :user_name, :age
  • validates_presence_of calls blank? for the validation
  • Validation failed for empty string or false boolean value

For model with association, user validates_presence_of to ensure the model contains at least 1 associated object (Ensure foreign key constraint)

   validates_presence_of :members, :message => "not exist in the DB"

Use the model name members instead of member_id

Mandatory boolean field

validates_inclusion_of :is_premium, :in => [true, false].
  • Cannot use validates_presence_of because false.blank? evaluates to true and therefore the validation will fail

Validate Unique Field(s)

Validate an attribute is unique

  validates_uniqueness_of :user_name, :case_sensitive => false, :message => "has already taken"

Validate the combined attribute1, attribute2, ..., attributeN attributes are unique

  validates_uniqueness_of :user_name, :scope =>[:birthday, :year_class]    # Unique for [user_name, birthday, year_class]

Validate with Regular Expression

Validate if it matches a regular expression

validates_format_of :code, :with => /\A[a-zA-Z]+\z/,

Validate Against a Set of Values

Validate against specific values

  # Accept only specific value
  validates_inclusion_of :shippment, :in => %w(FedEx USPS),
    :message => " only allow FedEx or USPS"

Validate against a range of value

  # Value range check
  validates_inclusion_of :fee,
    :in => 0..100, :message => "must be between 0 and 100"

Validate Against a Set of Excluded Values

Exclude certain value

  # Valid if exclude from a set of value
  validates_exclusion_of :format, :in => %w( pdf doc ) # Do not take pdf or doc format
  validates_exclusion_of :age, :in => 0...21           # Exclude people under 21

Numerical Value Validation

Allow certain a set of numeric values

  # More value check
  validates_numericality_of :age, :allow_nil => true,
    :greater_than => 17, :less_than_or_equal_to => 30, :only_integer => true

Length Check

Minimum/Maximum length check with error message

  validates_length_of :value, :minimum => 6, :too_short => "needs {{count}} long"
  validates_length_of :value, :maximum => 500, :too_long => "cannot exceed {{count}}"

  validates_length_of :value, :minimum => 6, :maximum => 12

Length range check

  validates_length_of :value, :in => 6..12

Check specific length

  validates_length_of :value, :is => 10, :wrong_length => "Wrong size"

By default, length checks the number of characters. To count by word say, use tokenizer to redefine a token

validates_length_of :value,
    :minimum   => 30
    :tokenizer => lambda { |text| text.scan(/\w+/) },

Date Validation

Date validation within a range

  validates_inclusion_of :birthday,
      :in => Date.civil(1980, 1, 1)..Date.today,
      :message => "must be between 1980 and now"

Validate Attribute is Set

Check if a field is set

  # Check field set by checkbox
  validates_acceptance_of :agreement                    # Valid if value is 1, otherwise invalid
  validates_acceptance_of :agreement, :accept => 'yes'  # Valid if value is "yes"

Validate Associated Objects

Triggered the associated objects to be validated

  # Validate the associated object also
  has_many :items
  validates_associated :items

For a many-to-many association, only validate from one end. Otherwise, there will be an infinite loop call

Confirm 2 Attributes contain the Same Value

Verify whether 2 different fields contain the exact same value

  • Useful for password & password confirmation field during user registration
      # Valid if :something and :something_confirmation have the same value
      validates_confirmation_of :password
      validates_presence_of :password_confirmation
    
  • validates_confirmation_of is true if the field is nil. To avoid that, use validates_presence_of to ensure it is not empty

Validation by a Custom Validation Class

Validate data by a validator class

validates_with NameValidator, :fields => [:first_name, :last_name]

  class NameValidator < ActiveModel::Validator
    def validate(record)
      record.name          # Access the ActiveRecord model
      options[:fields]     # Access the options passed to the validator
    end
  end
  • record storing the ActiveRecord object and the options storing the options passed to validates_with

Validates Attributes with Code Block

Use a code block to validate attribute(s) one at a time

# passing first name as attr and then last name
validates_each :first_name, :last_name do |record, attr, value|
    record.errors.add(attr, 'Cannot start with a number') if value =~ /\A[0-9]/
end

Common Rails Validation Options

  validates_length_of :user_name, :in => 6..12 , :some_option => some_value ...
Option Description
:allow_nil => true Allow to pass the validation if it is nil?
:allow_blank => true Allow to pass the validation if it is blank? (including nil?)
:message => "message" Error message
:on => :save Validate the field when insert/update a row (By default, validation is done on all DB operation)
:on => :create Validate the field when insert a new row
:on => :update Validate the field when update a row

Rails Conditional Validation

  validates_presence_of :user_name, :message => "require name for premium member", :if => :premium?

  def premium?
    self.premium
  end
Rails Validation Condition Description
:if => :some_method? Call some_method? for the condition
:if => "description.nil?" Evaluate the Ruby code
:if => Proc.new { |m| m.description.nil? } Invoke a Proc
:unless => ... Syntax similar to :if

Rails Data Validation

To verify whether an Active Record contains valid data explicitly

Account.create(:title=>"My title").valid?
Account.new.invalid?
  • valid? triggers data validation and return true if the data is valid

Trigger Rails Data Validation Implicitly

Some Active Record methods invokes validation automatically

Methods invoke validation explicitly create, create!, save, save!, update, update_attributes, update_attributes
No validation invoked decrement!, decrement_counter, increment!, increment_counter, new, toggle!, update_all, update_attribute, update_counters
  • Active Record methods that end with "!" raise an exception if the validation failed
  • To use save without validation: save(:validate => false)
  • Call valid? implicitly for methods that do not call validation automatically

Rails Data Validation Result

account = Account.new
account.valid?
account.errors
account = Account.new
account.save
account.errors
  • errors return an empty collection if validation has not been invoked
  • Return an empty collection if validation is invoked and found no errors
  • Otherwise, Return a collection containing the errors

Rails Programmatic Validation

Define methods to be executed when validating an ActiveRecord object

class Account < ActiveRecord::Base

  validate :account_name, :account_description

  def account_name
    errors.add(:user_name, "can't be blank") if user_name.blank?
  end

  def account_description
    errors.add(:description, "can't be blank") if description.blank?
  end
end

Re-using Rails Validation Code

Create a new re-usable validation rule (Code can be added as an initializer under config/initializers)

ActiveRecord::Base.class_eval do
  def self.validate_account_name(attr_name, value, options={})
    # Calling Out of the box validator
    validates_inclusion_of attr_name {:in => 1..n}.merge(options)
  end
end
 class SomeObject < ActiveRecord::Base
   validate_account_name :my_attr 10
 end

Adding Rails Validation Errors

def validate_something
  # Add error for a specific field
  errors.add(:user_name, "error message for the field user_name")
  errors[:title] = "Cannot be empty"

  # Append error for the object
  errors[:base] << "Object state is invalid"
end

Access Validation Error

Check the validation result of a particular field

account.errors[:title]
account.errors[:title].any?
  • errors[:title] returns a non-empty collection if the validation of :title fails

Check whether the collection is empty or not

account.errors.valid?

Accessing Validation Errors

# Return all error messages in an array
account.errors.full_messages

# Number of errors
account.errors.size

# Return nil if no validation error on user_name
# A string for single error
# Array for multiple errors
account.errors.on(:user_name)

Clear the error message

account.errors.clear

Display Validation Error Messages

Display all errors

<% form_for(@account) do |f| %>
  <%= f.error_messages %>

Customize error message

<%= f.error_messages :header_message => "Wrong account information",
  :message => "Please fix the following",
  :header_tag => :h3 %>
  • header_message: title for the error message
  • message: Header before the error message

CSS for the error message

public/stylesheets/scaffold.css
.field_with_errors {
  padding: 2px;
  background-color: red;
  display: table;
}

#errorExplanation {
  width: 400px;
  border: 2px solid red;
  padding: 7px;
  padding-bottom: 12px;
  margin-bottom: 20px;
  background-color: #f0f0f0;
}

#errorExplanation h2 {
  text-align: left;
  font-weight: bold;
  padding: 5px 5px 5px 15px;
  font-size: 12px;
  margin: -7px;
  background-color: #c00;
  color: #fff;
}

#errorExplanation p {
  color: #333;
  margin-bottom: 0;
  padding: 5px;
}

#errorExplanation ul li {
  font-size: 12px;
  list-style: square;
}

Generated HTML error message with the CSS

<form action="/accounts/1" class="edit_account" id="edit_account_1" method="post">

<div class="errorExplanation" id="errorExplanation">
  <h2>1 error prohibited this account from being saved</h2>
  <p>There were problems with the following fields:</p>
  <ul><li>User name  is mandatory</li></ul>
</div>

<p>
  <div class="field_with_errors"><label for="account_user_name">User name</label></div><br />
  <div class="field_with_errors">
    <input id="account_user_name" name="account[user_name]" size="30" type="text" value="" />
  </div>
</p>

Display Field Validation Error Side-by-Side

ActionView::Base.field_error_proc = Proc.new do |tag, instance|
  if instance.error_message.kind_of?(Array)
    %(#{tag}<span class="error-field">&nbsp;
      #{instance.error_message.join(',')}</span>)
  else
    %(#{tag}<span class="error-field">&nbsp;
      #{instance.error_message}</span>)
  end
end
  • Add a CSS style error-field manually in scaffold.css

Rails Model Callback

To add callback to listen to the Model life-cycle event

Model
class Account < ActiveRecord::Base
  validates_presence_of user_name

  # Call pre_check before validation
  before_validation :pre_check

  # Call a code block before creating a new row in DB
  before_create {|model| model.user_name = "anon" if model.user_name.blank?}

  protected
  def pre_check
    if (user_name == "admin)
      ...
    end
  end
end
Type of method call Validation Call-back
Creation before_validation
  before_validation_on_create
  after_validation
  after_validation_on_create
  before_save
  before_create
Updating before_validation
  before_validation_on_update
  after_validation
  after_validation_on_update
  before_save
  before_update
  after_update
  after_save
Destroying before_destroy
  after_destroy
Finder after_find (after load a DB record)
Other after_initialize (new or loading object from db)
Model calls that will trigger a callback
create, create! ,decrement!, destroy, destroy_all, increment!, save, save!
save(false), toggle!, update, update_attribute, update_attributes, update_attributes!
valid?
Calls triggering a finder callback
all, first, find, find_all_by_attribute, find_by_attribute, find_by_attribute!, last

Trim space before validation

Trim extra space before validation

before_validation :trim_space

private

def trim_space
  attribute_names().each do |name|
    if self.send(name.to_sym).respond_to?(:strip!)
      self.send(name).strip!
    end
  end
end

Rollback Model operation in a callback

To rollback the Model operation

  • Return false or raise an error in "before callback"
  • Raise an error (ActiveRecord::Rollback exception) in "after callback"

Conditional Callback

Add condition on when callback will be invoked
  # Calling a method to check the condition
  before_validation :pre_check, :if => :is_pre_check_needed?
  before_validation :pre_check, :unless => not :is_pre_check_needed?

  # Evaluate a string
  before_validation :pre_check, :if => ":is_pre_check_needed?"

  # Calling a Proc
  before_validation :pre_check, :if => Proc.new { |m| m.description.nil? }

  # Multiple conditions
  before_validation :pre_check, :if => :is_pre_check_needed?,
                                :unless => Proc.new { |m| m.description.nil? }

Callback Using a Object

Register a class in handling callback
class Account < ActiveRecord::Base
  after_destroy AccountCallbacks.new
end
Define the callback class
class AccountCallbacks
  def before_validation(model)
     ...
  end
end
Define the callback class using class method
class AccountCallbacks
  def self.before_validation(model)
     ...
  end
end

Rails Observer

Callback is designed to manipulate its own model data only. Observer is for business logic related with other models.

Add an observer of the model "Account"

% rails generate observer Account
app/models/account_observer.rb
class AccountObserver < ActiveRecord::Observer
  def after_create(model)
     ...
  end
end

To share observer for multiple models

Observer
class SomeObserver < ActiveRecord::Observer
  # Observe order and account
  observe :order, :account

  def after_create(model)
     ...
  end
end

To activate the observer, we need to register an observer

config/environment.rb file or config/environments/*.rb
# Activate an observer
config.active_record.observers = :account_observer