1. Skip to navigation
  2. Skip to content

The ELC Community Blog

A knowledge exchange on Ruby on Rails and Agile Development


DRY validates_inclusion_of with introspection

by on May 03, 2007

When developing a Rails application, it's not uncommon to require a user to select an item from a list. That list might be connected to a model by way of being an id in the database table the model represents.

For example, let us say the user must choose a country, level one administration region (i.e. state in the United States), and a city. Properly normalized, the result is three models and three tables. During rapid development, you may hastily write:

   1  validates_inclusion_of :country_id, :in =>
   2    Country.find(:all).collect {|country| country.id}
   3  validates_inclusion_of :state_id, :in =>
   4    State.find(:all).collect {|state| state.id}
   5  validates_inclusion_of :city_id, :in =>
   6    City.find(:all).collect {|city| city.id}

While the above validation services its purpose, to ensure that a valid country, state, and city is chosen, it is both repetitive and enormously slow.

Each time an instance of City is instantiated, for example, it must query the cities table and retrieve all records, then collect each id. ActiveRecord is a particularly slow portion of Rails, so minimizing the excessive creation of Active Record objects where possible can be beneficial.

Fortunately, we can both DRY and improve the performance of the validation.

   1  validates_each :country_id, :state_id, :city_id do |record, attrib, value|
   2  	begin
   3  	attrib.to_s.titleize.camelize.gsub(' ', '').constantize.find(value)
   4  	rescue ActiveRecord::ActiveRecordError
   5  	record.errors.add attrib, 'must be selected from list.'
   6  	end
   7  end

Above, validates_each is used for a custom validation. As indicated in the Rails documentation, we have a block that receives an instance of the record, the attribute in question, and its value.

Within a begin rescue block, the attribute is stringified, then Rails magic is employed such that the underlying classes, Country, State, and City can have their class finders invoked. If a record with the specified id is found, the validation is a success. Otherwise, we rescue an ActiveRecordError and add a message to the record's errors hash.


UPDATE: Mike Boone points out that the original code did not handle attribute names that were not a single word. I've updated the code to have a gsub call to remove the space that broke things in that case. Thanks Mike.

Comments

Add a comment


home | services | Ruby on Rails Development | code | blog | company