(For the impatient: skip directly to the `attribute_mapper` gem.)
In the past couple months, I’ve worked on two different projects that needed something like an enumeration, but in their data model. Given the ActiveRecord hammer, they opted to represent the enumeration as a has-many relationship and use a separate table to represent the actual enumeration values.
To a man with an ORM, everything looks like a model
So, their code ended up looking something like this:
class Post < ActiveRecord::Base belongs_to :status end class Status < ActiveRecord::Base has_many :tickets end
From there, the statuses
table is populated either from a migration or by seeding the data. Either way, they end up with something like this:
# Supposing statuses has a name column Status.create(:name => 'draft') Status.create(:name => 'reviewed') Status.create(:name => 'published')
With that in place, they can fiddle with posts as such:
post.status = Status.find_by_name('draft') post.status.name # => 'draft'
It gets the job done, sure. But, it adds a join to a lot of queries and abuses ActiveRecord. Luckily…
I happen to know of a better way
If what you really need is an enumeration, there’s no reason to throw in another table. You can just store the enumeration values as integers in a database column and then map those back to human-friendly labels in your code.
Before I started at FiveRuns, Marcel Molina and Bruce Williams wrote a plugin that does just this. I extracted it and here we are. It’s called attribute_mapper
, and it goes a little something like this:
class Post { :draft => 1, :reviewed => 2, :published => 3 } end
See, no extra table, no need to populate the table, and no extra model. Now, fiddling with posts goes like this:
post.status = :draft post.status # => :draft post.read_attribute(:status) # => 1
Further, we can poke the enumeration directly like so:
Post.statuses # => { :draft => 1, :reviewed => 2, :published => 3 } Post.statuses.keys # => [:draft, :reviewed, :published]
Pretty handy, friend.
Hey, that looks familiar
If you’ve read Advanced Rails Recipes, you may find this eerily familiar. In fact, recipe #61, “Look Up Constant Data Efficiently” tackles a similar problem. And in fact, I’m migrating a project away from that approach. Well, partially. I’m leaving two models in place where the “constant” model, Status
in this case, has actual code on it; that sorta makes sense, though I’m hoping to find a better way.
But, if you don’t need real behavior on your constants, attribute_mapper
is ready to make your domain model slightly simpler.