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
Person
religion_id
Position
Value Religion
Code Position
Last Name
ethnicity_id
Value Ethnicity
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
Person Last Name
Value
religion_id
Lookup
ethnicity_id
Code
Position lookup_type_id LookupType
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.