home icon contact icon rss icon

By: Matt Lins

Dissecting Rails Validations - Part I

The road from save to validation.

Rails validations seem simple on the outside, but have you ever taken the time to understand what's really going on? What actually happens when you do model.save? I ran into a problem one day and I decided to take a gander and figure out how it all works. It was a little difficult. The Rails core can be very daunting. With the mix-ins, aliases and the enormity of the ActiveRecord code base, one could spend hours trying to figure it out. Well, I did and I'm going to try to explain it.

To start, I have my Rails application froze to 1.2.3 and I can easily browse the code by looking in my vendors/rails/ directory(you could check it out of SVN if you desire, to follow along). All the files I mention will be relative to that path. I began my journey in the activerecord/validations.rb file. It contains a contains two ActiveRecord classes: ActiveRecord::Errors and ActiveRecord:RecordInvalid and a module: Validations, which contains another module: Validations::ClassMethods. Well, this file gave me a nice idea of how things like validates_presence_of works(which I'll explain later), but I wanted to know more. Specifically, during the save process, when do validations get called and from where?

Well, I scanned right over some clues in activerecord/validations.rb and hastily decided to go right into the heart of ActiveRecord: ActiveRecord::Base in activerecord/base.rb. I was puzzled when I found the #save and #save! methods.

1
2
3
4
5
6
7
8
9
10
11
12
13
# * No record exists: Creates a new record with values 
#   matching those of the object attributes.
# * A record does exist: Updates the record with 
#   values matching those of the object attributes.
def save
  create_or_update
end
      
# Attempts to save the record, but instead of just returning false if it couldn't happen, 
# it raises a RecordNotSaved exception
def save!
  create_or_update || raise(RecordNotSaved)
end

Naturally, I moved on to #create_or_update, to find:

1
2
3
4
5
def create_or_update
  raise ReadOnlyRecord if readonly?
  result = new_record? ? create : update
  result != false
end

Getting closer, but not quite what I'm looking for. This method checks and raises an exception if the record is read-only. Then it determines if the record is new and calls the appropriate method for a create or update.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# Updates the associated record with values matching those of the instance attributes.
# Returns the number of affected rows.
def update
  connection.update(
    "UPDATE #{self.class.table_name} " +
    "SET #{quoted_comma_pair_list(connection, attributes_with_quotes(false))} " +
    "WHERE #{connection.quote_column_name(self.class.primary_key)} = #{quote_value(id)}",
    "#{self.class.name} Update"
  )
end

# Creates a record with values matching those of the instance attributes
# and returns its id.
def create
  if self.id.nil? && connection.prefetch_primary_key?(self.class.table_name)
    self.id = connection.next_sequence_value(self.class.sequence_name)
  end

  self.id = connection.insert(
    "INSERT INTO #{self.class.table_name} " +
    "(#{quoted_column_names.join(', ')}) " +
    "VALUES(#{attributes_with_quotes.values.join(', ')})",
    "#{self.class.name} Create",
    self.class.primary_key, self.id, self.class.sequence_name
  )

  @new_record = false
  id
end

Where does validation come into play? We're already building SQL? Well this stumped me for about 10 seconds. Then I realized somewhere they must be mixing in the validation functionality. Logically, I headed back to activerecord/validations.rb to find what I overlooked:

1
2
3
4
5
6
7
8
def self.included(base) # :nodoc:
  base.extend ClassMethods
  base.class_eval do
    alias_method_chain :save, :validation
    alias_method_chain :save!, :validation
    alias_method_chain :update_attribute, :validation_skipping
  end
end

This code is aliasing the Base#save method to Validations#save_with_validation, which is mixed in just prior. To do so, it uses a built-in rails convenience method: #alias_method_chain. This happens when the module is loaded by using self.included.

I really enjoy the design of ActiveRecord. It is totally usable outside of Rails. Not only that, if you don't need validation then you simply don't load activerecord/validation.rb(Rails does this by default during initialization). But, just because you load the Validations module, doesn't mean you actually need to validate on save. As seen in the overridden methods below, you can simply pass false as a parameter.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# The validation process on save can be skipped by passing false. The regular Base#save method
# is replaced with this when the validations module is mixed in, which it is by default.
def save_with_validation(perform_validation = true)
  if perform_validation && valid? || !perform_validation
    save_without_validation
  else
    false
  end
end

# Attempts to save the record just like Base#save but will raise a RecordInvalid exception 
# instead of returning false if the record is not valid.
def save_with_validation!
  if valid?
    save_without_validation!
  else
    raise RecordInvalid.new(self)
  end
end

Well I think this is a good place to stop for Part I of this series. In the next post we will dig in to the actual validation process.

Daniel said

Apr 15, 2008 @ 11:55 PM

That was a bit interesting, thanks for the gander :)

ryan said

Apr 15, 2008 @ 11:55 PM

thanks for that. taught me something new.

RSS feed for comments on this post

Leave a Comment