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.