Thursday, February 28, 2008

AddSymbolicNames - Making constants out of records in static tables with ActiveRecord

Here's the problem: You've got a fairly complex database schema and you find yourself creating a lot of normalized tables. Then you find yourself writing application code that switches on the values of a normalized table. Then you find your application loading activerecord model after activerecord model to do comparisons. Then you think you need to build an in memory cache to save on lookups. Here's my quick solution that has worked for me in many situations: Say you have the following:
class State < ActiveRecord::Base
  has_many :cities
end

class City < ActiveRecord::Base
  belongs_to :state
end
(Assume State has a code column which stores the two letter state code: 'WA', 'CA', etc) Now somewhere in your application you want to write some code that only runs for cities in WA. You might have something like the following:
if city.state.code == 'WA'
# do something
end
So, activerecord is going to end up hitting the database for the state attribute so it can compare the code of that state to 'WA'. Now to avoid the database lookup to fetch the state model, you could of course use any number of caching mechanisms that exist (acts_as_cached, CachedModel). But it might be nice for something like state that doesn't ever change to be able to treat it more like a symbol and less like a model. So you could do something like this:
if city.state.code == State::WA
or even better
if city.state_id == State::WA
This last one avoids the lookup for state altogether and removes the string constant which could contain typos or change over time. Now, I deal with a bunch of these types of tables (static content except during new releases, <1000 records, lots of application logic around specific values), so I wrote this simple helper to generate namespaced symbols mapping a column to the id.
module AddSymbolicNames

  module ClassMethods
    
    def add_symbolic_names(opts = {})
      symbolic_name_attr = opts[:symbolic_name_attrib] || :symbolic_name
      value_attr = opts[:value_attrib] || :id
      
      find(:all).each do |r|
       class_eval %{
        if !defined? #{r.send(symbolic_name_attr)}
         #{r.send(symbolic_name_attr)} =
         #{r.send(value_attr)}
        end
       }
      end
    end
    
  end
  
  def self.included(base)
    base.extend(ClassMethods)
  end
  
end

ActiveRecord::Base.class_eval do 
  include AddSymbolicNames
end
Now with our previous example we can do this:
class State < ActiveRecord::Base
  has_many :cities
  add_symbolic_names :symbolic_name_attrib => :code
end

# somewhere in app-land
if city.state_id == State::WA
  # do some awesome washington specific logic
end
This creates symbols for every state that you have in the db (ie, State::WA, State::CA) that map to the id for that record. A few obvious restrictions: the :symbolic_name_attrib column must be unique and it also must not contain whitespace since it will become a symbol. It's basically like creating enums in c++ that reflect database values but that are set at run time. You could also do this if you felt like it:
class State < ActiveRecord::Base
  has_many :cities
  add_symbolic_names :symbolic_name_attrib => :code,
                     :value_attrib => :name
end
Now the constant State::WA will return the string "Washington" assuming the 'name' attribute of State returns the name of the state.

No comments: