Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Ruby on Rails Monkey Patching a Gem's Model

This might be silly, but I'm including a gem which represents all the models I need for my project. I want to add a method, to_custom_string to one of those models, Person.

I was attempting to do this via (following this example): config/initializers/extensions/person.rb

Which contained something like:

class Person < ActiveRecord::Base
  def to_custom_string
    address.street.to_s
  end
end

The Person class in the gem has a has_one :address association.

The problem I was experiencing was that this patch seems to override the Person class from the gem, instead of patching it. What's crazy is that this override behavior was only experienced via rake (all of the associations declared in the Person class from the gem are lost).

My rake task was something like:

namespace :convert
  task :all_persons => :environment do
    Person.where(:param => value).includes(:address).find_in_batches(:batch_size => 2000) do |persons|
      persons.each do |person|
        puts person.to_custom_string
      end
    end
  end
end

calling bundle exec rake convert:all_persons gave me:

Association named 'address' was not found; perhaps you misspelled it?

But copying and pasting the code in the rake task into rails console worked fine.

My current solution is to copy the code for Person from the gem into my app/models directory, and have my to_custom_string method there, which I know is wrong.

Can someone please explain why a) irb preserved my Person associations, but rake did not, and b) how I can get rake to cooperate?

Thank you!

like image 631
Allen T. Avatar asked Aug 16 '12 20:08

Allen T.


1 Answers

First of all instead of reopening the class I would create a Module and include it into the Person. So it would look like that

  module CustomString
    def to_custom_string
      address.street.to_s
    end
  end

  Person.send(:include, CustomString)

Also it seems like the Person model is not yet available at the point of running the initializer. You may want to put this in your application.rb if still doesn't work.

  config.railties_order = [ModelEngine::Engine, :main_app, :all]

I guess the reason why it works in irb and not in rake is because they look up classes differently. Irb (which I believe you run by running rails console) loads all the classes at once therefore it loads the classes from engine, then it runs the initializer where you have the classes from engine already defined. I guess (though I'm not sure) Rake in development mode uses lazy loading of constants. So it doesn't load all the classes at the very beginning and only when it finds a constants that is undefined. Then it starts looking for a file that may define that constant. Since you put some Person in initializer it doesn't look up the engine's model at all cause at the point it sees Person it has the Person definition already. That's why the inclusion of module instead of reopening the class may help -> it enforces that it will lookup the Person constant from engine.

like image 143
Piotr Jakubowski Avatar answered Oct 03 '22 18:10

Piotr Jakubowski