Recently I came across an issue where I had several tables in my rails application that needed options. I really didn't want to just add options fields to each of them for obvious reasons (the options might change, etc). In the code of the project, the options were represented as a hash unique to each individual user ie: { :display_creator => true, :position => 1.1 }. So in order to abstract some of this away I decided to go with a polymorphic table in ActiveRecord, which works like so:
class Option < ActiveRecord::Base
belongsto :owner, :polymorphic => true
end
class User < ActiveRecord::Base
hasmany :options, :as => :owner
end
class Operation < ActiveRecord::Base
has_many :options, :as => :owner
end
Pretty basic stuff, now every time I reference the options method from an individual user I get back a series of option objects associated with my users. If I reference the options method from my Operation object, I'll get back only the options associated with that particular operation. Rails does this by looking for two columns you must create when you create your model
With this solution in place I was off to the races, however I noticed that I was having to convert all these options back into a hash (they were essentially being stored as a key value pair of strings in the db). Naturally this is an action that would be great to throw into the models, however I began to realize that I'd have to repeat this option object to hash method in each model if I wanted to store it there. And that's certainly not very DRY. Remembering some work I had done on the govkit library, I decided to use an acts_as reference condensing all the attributes of a model that are associated with having options down and creating a method that could add them to any individual model with a single call. (Note the Option class is unchanged)
config/initializers/activerecordaddons.rb
module ActiveRecordAddons
def hasoptions
classeval do
hasmany :rawoptions, :as => :owner, :classname => "Option"
def options
opts = {}
rawoptions.each |opt|
opts[opt.key.to_sym] = converter(opt.value)
end
opts
end
private
def converter val
Integer(val)
rescue ArgumentError
Float(val)
rescue ArgumentError
val
end
end
end
end
ActiveRecord::Base.extend ActiveRecordAddons
You'll note that now when you call options, it's actually referencing the options method which parses out the rawoptions from the database and returns a hash. If I were doing this from an independant library, I would have packaged this all up in a gem and included the gem in my gemfile, however I'm only using this from my one project, so I instead wrote the code out and added it to an activerecordaddons.rb file in the config/initializers directory. This ensured that the method was monkey patched onto the ActiveRecord::Base parent before the models were instansiated. So now ActiveRecord::Base and all it's children have this hasoptions method available, which will add in all the necessary code to make the models able to store their options hashes. So now my models look like so:
class User < ActiveRecord::Base
hasoptions
end
class Operation < ActiveRecord::Base
hasoptions
end
Nice and DRY, and now any changes I need to make to how these models deal with their options can be done from one place.
Hopefully someone else finds this helpful. Enjoy!
That's great for reading options. But how do you save them back to options.
Hmmm... I'd probably define a options= method to iterate through the hash and update/add the options to the object, so that you can maintain the nice User.first.options = { :key => "value" } format. Though given that the = operator is universally understood to set the value of something I'd probably also add an add_options method to just append options without deleting any or resetting the full object.
Something like this: https://gist.github.com/1158636