Generic Lookups in Rails

A brief walk thru time and space ...
Words and Music by Bill Siggelkow

About Me
My last name is pronounced Siggelkōw, not cow. Please ignore the pesky “w” on the end. 1985 Graduate of the North Avenue Trade School. Reformed Fortran to C to Java Programmer. Author of the Jakarta Struts Cookbook (O’Reilly) Ruby/Rails for 2 years -- making a living at it since May ’07. Uncanny ability to store and recall loads of worthless information in my brain.

Lookups
A person can have any number of “demographic” attributes that are “looked up” such as marital status, religion, and ethnicity. We want the set of valid values for each of these attributes types to be specified and maintained in our database. Implementing in code (using constants) requires redeployment to the server. Using the database gives us a place to hang “administration”.

Lookups
Each lookup of a lookup type consists of: value (e.g. married), code (optional -- could be used for legacy integration) position (for support of acts_as_list) There is no additional data about the association between an entity and the lookup value. For example, we do not need to track the number of years a person has been married ...

Data Model v0.1
Value Code Marital Status First Name marital_status_id Position

Value Religion Code Position

Person Last Name

religion_id

ethnicity_id Ethnicity

Value Code Position

Problems with Data Model v0.1
If we have new types of attributes (e.g. hair color), we have to create a whole new table. The model (and, therefore, the code) is not very DRY. Our Rails app will have umpteen models with identical attributes. (Even utilizing extension it’s still a lot of classes).

Data Model v0.2
marital_status_id First Name Value

religion_id

Person Last Name

Lookup

Code

ethnicity_id lookup_type_id LookupType

Position

Name

lookup.rb
# :lookup_type - the type of lookup # :value - a valid value # :code - an optional code (e.g. a numeric code used by some third-party) # :position - orders the lookups scoped to a given :name. (This permits # specific ordering of the lookup values for drop-downs and the like.)

class Lookup < ActiveRecord::Base belongs_to :lookup_type acts_as_list :scope => :lookup_type end

lookup_type.rb
class LookupType < ActiveRecord::Base has_many :lookups, :order => 'position', :dependent => :delete_all do def by_value(value) find(:first, :conditions => {:value => value}) end end validates_uniqueness_of :name validates_presence_of :name end

person.rb (v0.1)
class Person < ActiveRecord::Base belongs_to :marital_status, :class_name => ‘Lookup’, :foreign_key => ‘marital_status_id’ belongs_to :religion, :class_name => ‘Lookup’, :foreign_key => ‘religion_id’ .... end

Person v0.1 Problems

We DRYed up the lookups but now our Person class is not DRY. “Person belongs to marital status” sounds weird. A person should have a marital status, not belong to one.

Solution ... Monkey Patch Rails!
module ActiveRecord module Associations module ClassMethods def has_lookup(association, options = {}) options.merge!( :class_name => 'Lookup', :foreign_key => "#{association}_id" ) self.belongs_to association, options end alias has_a has_lookup alias has_an has_a end end end

person.rb (v0.2)

class Person < ActiveRecord::Base has_a :marital_status has_a :religion has_an :ethnicity .... end

A View Helper for Drop-downs
def select_from_lookup( object, method, options={}, html_options = {}) lookup_type = LookupType.find_by_name(method) choices = lookup_type.lookups.collect {|l| [l.value, l.id] } select( object, "#{method}_id", choices, options, html_options ) end -------------------------------------------------------------------------<%= select_from_lookup 'person', 'marital_status' %>

Questions and Comments

I’d like to say thank you on behalf of the group and ourselves and I hope we’ve passed the audition.