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