Yes. it has been a long time since I’ve posted anything, but I’m sure I’ll want to refer to this in the future.
While developing some new functionality for TheBigFork, I found an opportunity to take advantage of the mysterious “method_missing” method in my ActiveRecord models. I have two models: Website and Setting. A website has many settings. Nothing earth-shattering there. To simplify the explanation, let’s just say I want to be able to load a value from the settings table with something like this:
Where the “site_name” comes from the related settings. The longer, built-in way to do this is:
website.settings.where(:name => 'site_name').first.value
Okay, so the “method_missing” approach would look something like this:
class Website < ActiveRecord::Base has_many :settings def method_missing(method) self.settings.where(:name => method).first.value end end
(I really need to figure out a better way to post code in WordPress. Maybe it is time to move this blog to my own server where I can manage plugins. Anyway…)
That approach sort-of works. It seems to work better in some versions of rails than others. Some problems show up because “website.respond_to?” returns false for these pseudo methods. That could be fixed by adding this to the Website class:
def respond_to?(method) if self.method.to_s != 'settings' && self.settings.where(:name => method) true else super end end
But that still gave me all sorts of “stack level too deep” errors. I stumbled upon Jay Field’s post, Replace method_missing with dynamic method definitions. Just what I needed. Except it wasn’t a Rails-specific solution. Just ruby in general. Apparently, classes that inherit from ActiveRecord::Base may or may not load with your own initialize method. So, I used the after_initialize hook like so:
class Website < ActiveRecord::Base has_many :settings after_initialize :dynamic_methods def dynamic_methods self.settings.each do |setting| (class << self; self; end).class_eval do define_method setting.name.to_sym do |*args| self.__send__("get_value", setting.name, *args) end end end end def get_value(name) self.settings.where(:name => name).first.value end end
Works like a charm. It even answers correctly to the “respond_to?” for each setting.
Now, it is a bit expensive to dynamically create lots of methods every time you initialize a new instance, so I probably wouldn’t recommend this approach if you have tons of settings for each website. However, in my application, I actually have a third model involved (Brand) which shares settings among websites. The website actually caches the values, and this solution works nicely.