MonkeyPatching ActiveRecord::Base class


#1

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
is my code in the active_record_extensions.rb file that I created in the
RAILS_ROOT/lib directory:

module ActiveRecord

class Base
def self.search(search, current_page)
if search.blank?
paginate(:all, :page => current_page || 1, :per_page => 5)
else
paginate(:all, :conditions => [“name LIKE ?”, “%#{search}%”],
:order => ‘name’,
:page => current_page || 1, :per_page => 5)
end
end
end

end

Note that paginate method comes from the will_paginate plugin installed
in vendor/plugins directory.

This does not work. My program is having trouble with the paginate
method from will_paginate plugin being called in the code shown above?

This seems to be a load_path issue? One way to solve this is to install
will_paginate as a gem and then require it before calling paginate? But
is there a way to make with work as a plugin as I have it currently for
creating this generic routine?

Thanks for your time in advance.

Bharat


#2

On Mar 23, 2009, at 5:09 PM, Bharat R. wrote:

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.


#3

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


#4

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!


#5

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:inmethod_missing_without_paginate’
/vendor/plugins/will_paginate/lib/will_paginate/finder.rb:170:in
method_missing' /app/controllers/pizzas_controller.rb:17:inindex’
/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:inperform_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:inperform_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:inmeasure’

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


#6

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:inmethod_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:inmethod_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?


#7

On Mar 25, 3:04 pm, Bharat R. removed_email_address@domain.invalid
wrote:

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)


#8

Thank you Fred and Greg. You both are right. I had a similar load
configuration block as Greg shows here in my environment file in the
application where it worked fine and had forgotten all about it. For
this little application, I put the following statement at the end of
config/environment.rb:

require “#{RAILS_ROOT}/lib/active_record_extensions.rb”

And everything works fine. I was under the impression that everything
in the lib directory gets loaded automatically which is apparently an
incorrect assumption.

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?

Bharat


#9

On 25 Mar 2009, at 16:11, Bharat R. wrote:

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


#10

“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


#11

On Wed, Mar 25, 2009 at 10:04 AM, Bharat R.
removed_email_address@domain.invalid wrote:

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.

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.


Greg D.
http://destiney.com/


#12

On Mar 25, 8:22 pm, Bharat R. removed_email_address@domain.invalid
wrote:

“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?

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


#13

“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.