company-logo

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|
          Column.new name.to_s, nil, type.to_s, false
        end
      end
    end
  end
end

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

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
end

The type of the column is needed because ActiveRecord will automatically typecast values for you, so if you did FooSearchCriteria.new(:page => '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 = FooSearchCriteria.new(session[: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.

  1. Dan Reply

    Looks pretty cool. Quick question though: doesn’t it break if you use it twice in the same app? The code looks like it would be better off with eigenclass instance variables instead of class variables.

  2. Anthony Caliendo Reply

    It will work with multiple classes because of how “class_variable_set” and “class_variable_get” work; when they execute, their scope will be unique for each class.

    If you were to directly access “@@acts_without_database_columns” instead of through the reflection methods, then it would break.

  3. jc Reply

    Try this one out:
    github.com/jcnetdev/active_record_without_table

    It uses a different Parent object, however, I think it’ll work better for your purposes.

  4. D James Reply

    I just want to point out a situation this is directly applicable to: credit card processing. Smaller online merchants don’t want to store credit card data in their databases for liability reasons. Still, when developing for such merchants, we need to be able to take advantage of ActiveRecord validation and such.

    I feel we’d all benefit from a tested example that uses ActiveRecord and acts_without_database side-by-side to offer a good solution for venders. I’ve spent hours trying to piece together a solid credit card processing application – and I know I’m neither the first nor will I be the last (hint hint).

Leave a Reply

*

captcha *