logo
  • Jobs
  • About Me
  • Contact
  • Home
« Site update
Capistrano Deploy Error »

Providing ActiveRecord validations for non-database models

Posted March 18th, 2008 by Matt Berther

The validations provided by the Rails framework are very powerful and provide a very easy, no cost way of validating forms prior to submitting them to the database. You might see the validations in a class this way:

class User < ActiveRecord::Base
  validate_presence_of :name
end

If there is not a name entered on the form when creating/updating the user, the save will fail with an error that can be displayed to the user.

I was hoping to leverage this same technique for some other forms that I had in my Rails application. These forms did not have a corresponding model in the database, as they were models that described sending an email. The goal was to provide for some required fields and then check whether or not the form was valid prior to sending the email.

My first pass at this looked like this:

class ContactInformation < ActiveRecord::Base
  attr_accessor :name
  attr_accessor :email_address
  attr_accessor :body
 
  validates_presence_of :name
  validates_presence_of :email_address
  validates_presence_of :body
end

and in the controller:

def send_mail
  @contact_info = ContactInformation.new(params[:email])
  Notification::deliver_contact_form(@contact_info) if @contact_info.valid?
  # error checking omitted for brevity
end

This technique did not work, since ActiveRecord makes a database call to define the accessors when valid? is called. I needed something a little different, so I looked to Google. I found a solution here that almost worked for me. What didnt work for me was that in the first piece of code, the behavior was overwritten for *all* ActiveRecord::Base, meaning that the rest of my application would not have database access with ActiveRecord. The second solution on the page seemed way more complicated than I felt that it needed to be. After all, the first solution worked. Using the first technique, I just needed to find a way to limit scope.

A little Ruby metaprogramming led me to this, which I think is an excellent solution to this problem (which I put in $RAILS_APP/lib/active_form.rb).

module ActiveForm
  def self.included(base)
    base.class_eval do
      alias_method :save, :valid?
      def self.columns() @columns ||= []; end
 
      def self.column(name, sql_type = nil, defaults = nil, null = true)
        columns << ActiveRecord::ConnectionAdapters::Column.new(name.to_s, default, sql_type, null)
      end
    end
  end
end

Essentially, what we’re doing is using Module.included and Module.class_eval methods to execute code whenever the ActiveForm module is included in another class or model. When the class is evaluated, we alias the save method to the valid method and add a method that allows are model to register columns. This method simply mocks out an ActiveRecord column (without connecting to the database), which basically fools ActiveRecord into thinking there actually is a database table.

The model class is modified slightly from the example above. In addition to including the ActiveForm module, which provides us this functionality, the attr_accessor methods are changed to the column method described above. The model now looks like this:

class ContactInformation < ActiveRecord::Base
  include ActiveForm
 
  column :name
  column :email_address
  column :body
 
  validates_presence_of :name
  validates_presence_of :email_address
  validates_presence_of :body
end

The controller code works as I initially had written it. And, I get all the joys of the ActiveRecord validations without having a database table for information I dont want to save in the database.

This entry was posted on Tuesday, March 18th, 2008 at 10:37 am and is filed under Uncategorized. You can follow any responses to this entry through the RSS 2.0 feed. You can leave a response, or trackback from your own site.

Sean
June 13th, 2008

def self.column(name, sql_type = nil, defaults = nil, null = true)

“defaults” should be “default” or else it returns an error.

<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>
-->

Social
  • mattberther on twitter
Syndication
Archives
  • August 2008
  • June 2008
  • May 2008
  • April 2008
  • March 2008
  • February 2008
  • January 2008
  • December 2007
  • November 2007
  • October 2007
  • September 2007
  • August 2007
  • July 2007
  • June 2007
  • May 2007
  • April 2007
  • March 2007
  • February 2007
  • January 2007
  • December 2006
  • November 2006
  • October 2006
  • September 2006
  • August 2006
  • July 2006
  • June 2006
  • May 2006
  • April 2006
  • March 2006
  • February 2006
  • January 2006
  • December 2005
  • November 2005
  • October 2005
  • September 2005
  • August 2005
  • July 2005
  • June 2005
  • May 2005
  • April 2005
  • March 2005
  • February 2005
  • January 2005
  • December 2004
  • November 2004
  • October 2004
  • September 2004
  • August 2004
  • July 2004
  • June 2004
  • May 2004
  • April 2004
  • March 2004
  • February 2004
  • January 2004
  • December 2003
  • November 2003
  • October 2003
  • September 2003
  • August 2003
  • July 2003
  • June 2003
  • May 2003
  • April 2003
  • March 2003
Jobs
mattberther.com © 2003 - 2008