Search code examples
ruby-on-railsrubymodelmetaprogramming

before and after method call on models in ruby on rails, do "thing"


I'm pondering a way to wrap method calls on models in RoR. (The reason for this being a custom model caching system that would allow me to do a lot of work behind the scenes on data being created by some large models that my clients are using without having to constantly load those models and parse ALL the very fat data to get the output. I could just send the output already calculated and stored in a different place and a delayed job behind the scenes that does the calculation and stores it isn't really an option due to how the data is collected or the egregious volume in question.. Its hard to explain.)

So given that I have an instance of this extremely simple class;

class Person < ApplicationRecord
  attr_accessor: :name
  has_many :books
  has_many :friends
end

How would I go about writing something that allows me to programmatically "intercept" ALL (or all except those I specifically say not to) method calls to a given class.

So if someone does this;

bob = Person.find 1
puts bob.name
puts bob.friends.count

I could have this system know to do something before .name is called or before .friends is called. Something like checking to see if we already have an answer to this question somewhere else..

I was looking at using a before_hook and override the method_added hook to prepend my code per this question: call before methods in model on ruby but the issue was it works for .name but not for .friends. I assume this is due to some rails magic as friends just gives you the active record call per normal without actually executing the hook..

I'd love to be able to do something like;

class Person < ApplicationRecord

  include CustomCachableThingy
  before_hook :friends, :name, except: :books

  attr_accessor: :name
  has_many :books
  has_many :friends
end

Again this is a very simple example. The models in question are way too big and have way too much data. So trying to find something I can make work for my client.

Thoughts?


Solution

  • If you really want to wrap every method call (or even the ones defined by the target class itself), you can define a module that does 2 things,

    • Get all the instance_methods and wraps them with your custom code
    • Override the method_added method call and wrap every new method that is added with the same custom code

    A good example using this strategy is answered here. You can customize this to take additional except configuration and exclude those methods.

    The meta-programming going on here is to rewrite each method as it is injected into the instance.


    Another option is to wrap only specific method calls with your custom code, if you know which ones you'll need to cache. It will be less overhead and cleaner implementation.

    For implementation, you can use the same meta-programming approach as described in the first example. One more approach can be to add an alias to the original method and rewrite the method to include your hook. Something like,

    def add_before_hook(method_name)
      class_eval do
        without = :"#{method_name}_without_before_each_method"
        alias_method without, method_name
    
        define_method method_name do |*args, &block|
          puts 'called before'
          send without, *args, &block
        end
      end
    end
    

    The problem with the second approach is that it pollutes your method namespace by adding a duplicate method for each of your instance methods.