Search code examples
ruby-on-railsactiverecordmonkeypatching

Why does monkey patching methods on ActiveRecord::Base redefine unrelated methods on Model.count


When I monkey patch ActiveRecord::Base with class methods, the methods are inherited by the different model ActiveRecord_Relation classes (like User::ActiveRecord_Relation) and can be called on instances of specific active record relations. But this causes some unexpected behavior when making active record calls for the original model.

Here's a trivial example:

User.count
=> 3544

users = User.where("created_at > ?", 1.month.ago)
users.count
=> 174

class ActiveRecord::Base
    def self.monkey_foo(options = {}, &block)
        User.count
    end
end

User.monkey_foo
=> 3544

Book.monkey_foo # Another model
=> 3544

users.monkey_foo
=> 173 # This is the count of the users relation, not the User model

Book.where("created_at > 1.year.ago").monkey_foo
=> 3544 # Issue only affects User model relations

What is causing this behavior?

I know that monkey patching like this is a pretty bad idea for anything serious. I accidentally discovered this behavior and I'm very curious to know why it's happening.


Solution

  • The key to this question is in delegation.rb

    Basically this has the follow method missing implementation for Relation (simplified slightly for brevity)

    def method_missing(method,*args,&block)
      scoping { @klass.public_send(method, *args, &block) }
    end
    

    (@klass is the active record class the relation belongs to)

    The scoping method sets the class' current_scope for the duration of the block. This contains things like where clauses, sorts etc. This is what allows you to call class methods on relations and have those class methods operate on the scope defined by the relation.

    In the book case this is still happening, however the scoping is happening to Book but the query is against User so the scoping doesn't change the result.