acts_without_database: Leverage ActiveRecord with Non-Database Backed Objects

ActiveRecord comes with a lot of nice things that aren't really dependent on having a database backed model. The most obvious example of this is the validation framework baked into ActiveRecord. Also, there are several plugins which add some useful behavior to ActiveRecord objects but don't rely on having a database.

In the future, I believe the rails team aims to make certain things more modular and easier to pull out and use separately from ActiveRecord (validation being one of those). However, if you are working on a project which is locked into a certain version of rails or you're impatient, there is a very simple plugin which can solve your needs.

Enter acts_without_database...

ActiveRecord has a very nice design decision which limits the database interaction to just loading the column definitions for a table/object (except, obviously, when querying or inserting/updating). This allows us to override the method which loads the columns to return some predefined column definitions.

Why would someone need to do this? Well, the problem I have run into project after project is advanced search. Often, you will have some additional fields from your model in order to allow a more powerful search (such as limiting to any combination of types in an STI situation, or allowing negative presence text search by using separate fields). You'll also want to provide some validation ("you must enter a search query", etc.). By using the active_record_defaults plugin, you may also want to provide some explicit way of defaulting search parameters without messing around with a constructor. You really want to use an ActiveRecord object, but it doesn't always make sense to have a database back this model (there are cases where you might want to, but let's ignore those for now).

In these cases, I have used a custom plugin. Drop the following into your lib directory (or config/initializers if you prefer and are using a rails version that allows it).

module ActsWithoutDatabase
  Column = ActiveRecord::ConnectionAdapters::Column

  def self.included(base)
    class << base
      def columns
        class_variable_get(:'@@acts_without_database_columns').collect do |name, type|
 name.to_s, nil, type.to_s, false

class ActiveRecord::Base
  def self.acts_without_database(columns = {:id => :integer})
    class_variable_set :'@@acts_without_database_columns', columns
    include ActsWithoutDatabase

This is pretty simple and doesn't do any caching on the columns, so if you want to optimize it you can easily add some caching for it. I mainly want to get the concept across as simply as possible.

Obviously, if you attempt to invoke #find, #save, or #update then you will get an error. I am against overriding these to silently fail, as I believe a test should fail if someone is attempting to hit the database with a model like this. I do, however, normally override the find method to invoke the actual find logic (whether using ActiveRecord, a full text search solution, or even some remote webservice call).

Now, you can do the following without a database:

class FooSearchCriteria < ActiveRecord::Base
  acts_without_database :page => :integer, :per_page => :integer, :query => :string

  defaults :page => 1, :per_page => 10

  validates_presence_of  :query
  validates_numericality_of :page, :per_page, :only_integer => true, :greater_than => 0

The type of the column is needed because ActiveRecord will automatically typecast values for you, so if you did => '1').page you would get a Fixnum back (handy when dealing with form inputs).

You can now call valid? on your object in the controller to perform the validations.

Need to save the search query in the session? Doing so is trivial, as you have the ActiveRecord method #attributes. Now you can do the following:

session[:foo] = @foo.attributes

And then reload the criteria by doing the following:

@foo =[:foo])

I have also found this technique immensely useful when writing stand-alone units tests for libraries/plugins which add behavior to ActiveRecord objects; you can test this code without needing a database or making the test dependent on some outside model class.