MonkeyPatching ActiveRecord::Base class

I am trying to monkey-patch the ActiveRecord::Base class to
incorporate a generic search class method so that it can be used by all model classes which need this functionality. Since model classes directly inherit from ActiveRecord::Base and unlike controllers and helpers, do not have an ancestor class defined, I think I am forced to open the ActiveRecord::Base class and patch it? May be I am wrong. Anyway,
here

Why not create your method as a module and then include it into the
classes you need it? Look at any of the plugins out there that are of
the "acts_as_xxxx" variety which add methods to the model they are
called from... just follow those examples and you shouldn't need to
directly modify AR::Base like this.

Thanks Phillip. I will follow your advice and see if I can do it.

Why not create your method as a module and then include it into the classes you need it? Look at any of the plugins out there that are of the "acts_as_xxxx" variety which add methods to the model they are called from... just follow those examples and you shouldn't need to directly modify AR::Base like this.

Further, adding a method to a class is a perfectly reasonable solution, and is not "monkey patching". That's when you change a preexisting method, simply because it offered no hook for you to override.

In order from sublime to icky...

  - delegate to the behavior you need   - include/extend the behavior   - inherit the behavior   - add the behavior to your base class   - trigger the behavior with an 'if' statement   - monkey-patch the behavior into a closed class

Yes, folks, the lowly 'if' statement is the second-worst option in Object Oriented programming. Use it with extreme caution!

Could not get to it yesterday due to other work. Am still struggling with it. There is something basic that I do not understand. I have looked at acts_as plugins (list and tree for instance). I am afraid that is not going to help me. Here is the thing that I do not get:

1. The Rails lib directory contains reusable code that can be used anywhere in that particular Rails application. Following that basic concept, I created a file called active_records_extensions.rb in it which has the following slightly modified code from the previous posting:

module ActiveRecord

  class Base

    class << self

      def search(search, current_page)         if search.blank?           WillPaginate::Finder::paginate(:all, :page => current_page || 1, :per_page => 5)         else           WillPaginate::Finder::paginate(:all, :conditions => ["name LIKE ?", "%#{search}%"], :order => 'name',             :page => current_page || 1, :per_page => 5)         end       end     end

  end

end

Now search is supposed to be a class method since it is called from the index method of a controller like this:

  def index     @pizzas = Pizza.search(params[:search], params[:page])   end

That is why I am defining the search method as a class method for ActiveRecord::Base class as shown above. I have done this sort of thing many times before without any problems. The twist this time is that the "Search" method is calling the paginate class method of will_paginate plugin.

First, I tried requiring the will_paginate class method, but that did not work. Next, I tried giving the full path for the paginate class method as shown above in the code. Still, I get the same error. Here is the stack trace snippet.

NoMethodError (undefined method `search' for #<Class:0xb7143b6c>):     /usr/local/lib/ruby/gems/1.8/gems/activerecord-2.1.2/lib/active_record/base.rb:1672:in `method_missing_without_paginate'     /vendor/plugins/will_paginate/lib/will_paginate/finder.rb:170:in `method_missing'     /app/controllers/pizzas_controller.rb:17:in `index'     /usr/local/lib/ruby/gems/1.8/gems/actionpack-2.1.2/lib/action_controller/base.rb:1166:in `send'     /usr/local/lib/ruby/gems/1.8/gems/actionpack-2.1.2/lib/action_controller/base.rb:1166:in `perform_action_without_filters'     /usr/local/lib/ruby/gems/1.8/gems/actionpack-2.1.2/lib/action_controller/filters.rb:579:in `call_filters'     /usr/local/lib/ruby/gems/1.8/gems/actionpack-2.1.2/lib/action_controller/filters.rb:572:in `perform_action_without_benchmark'     /usr/local/lib/ruby/gems/1.8/gems/actionpack-2.1.2/lib/action_controller/benchmarking.rb:68:in `perform_action_without_rescue'     /usr/local/lib/ruby/1.8/benchmark.rb:293:in `measure'

I am hoping someone can help me understand what is it that I am missing. I am doing this project to further my knowledge about designing reusable code in Rails and there is something fundamental that I am not able to grasp.

As always, thanks for your time.

Bharat

I have reduced the problem down to the following: whatever method I define in the lib directory under active_record_extensions.rb, is not being loaded by rails. This has worked perfectly for me in a large Rails 2.1.0 application. So here is the problem:

RAILS_ROOT/lib/active_record_extensions.rb:

module ActiveRecord

  class Base

    class << self

      def hello         puts "Hello from active record"       end

    end

  end

end

I start the server. I load Rails Console and try to invoke hello method on any model class. I get exactly the same error as shown below on Pizza class for example:

bruparel@bcr-d810:~/exp/pizzeria-3$ ruby ./script/console Loading development environment (Rails 2.1.2)

Pizza.hello

NoMethodError: undefined method `hello' for #<Class:0xb784607c>   from /usr/local/lib/ruby/gems/1.8/gems/activerecord-2.1.2/lib/active_record/base.rb:1672:in `method_missing_without_paginate'   from /home/bruparel/exp/pizzeria-3/vendor/plugins/will_paginate/lib/will_paginate/finder.rb:170:in `method_missing'   from (irb):1

CrustType.hello

NoMethodError: undefined method `hello' for #<Class:0xb72087dc>   from /usr/local/lib/ruby/gems/1.8/gems/activerecord-2.1.2/lib/active_record/base.rb:1672:in `method_missing_without_paginate'   from /home/bruparel/exp/pizzeria-3/vendor/plugins/will_paginate/lib/will_paginate/finder.rb:170:in `method_missing'   from (irb):2

Does anyone know what is going on?

Does anyone know what is going on?

That file's not going to magically load itself. you need to require it (eg from an initializer in config/initializers)

I found the same problem when upgrading an app to 2.3.2 yesterday. I fixed it using

Dir.glob( "#{ RAILS_ROOT }/lib/*.rb" ).each { |f| require f }

in config/environment.rb.

Question is: what is the significance of the "lib" directory then in a Rails environment? If it just gets added in the Rails load path and nothing else then it leaves me scratching my head. Either don't put
in the load path automatcially or if we do then load whatever is in there automatically too. Don't you think Rails is at odds with itself here?

Except for the production mode stuff, nothing gets loaded
automatically (the const_missing hook can make it seem that way
though. The difference with a file like the one you had is that there
isn't a top level constant one would naturally refer to)

Fred

"The difference with a file like the one you had is that there isn't a top level constant one would naturally refer to)"

Thanks for the explanation Fred. But I don't understand this. Can you give me a simple example?

Bharat

If what you were doing was providing a class that other bits of your app use then you might have discombobulator.rb which defined Discombobulator, and bits of your app might do Discombobulator.discombobulate. When they did that and if the file had not been loaded yet then this would trigger const_missing (since there is no Discombobulator constant). Rails' response to this is to search for a file called discombobulator.rb and try and load it.

However when you have a file in lib that is just adding a method to some existing class or overriding something then there is no constant like Discombobulator that will trigger loading of the file.

Fred

"However when you have a file in lib that is just adding a method to some existing class or overriding something then there is no constant like Discombobulator that will trigger loading of the file."

Got it. Thanks.