views:

998

answers:

3

I have library code that overrides Ar's find method. I also include the module for all Association classes so both MyModel.find and @parent.my_models.find work and apply the correct scope.

I based my code off of will_paginate's:

a = ActiveRecord::Associations
returning([ a::AssociationCollection ]) { |classes|
  # detect http://dev.rubyonrails.org/changeset/9230
  unless a::HasManyThroughAssociation.superclass == a::HasManyAssociation
    classes << a::HasManyThroughAssociation
  end
}.each do |klass|
  klass.send :include, Finder::ClassMethods
  klass.class_eval { alias_method_chain :method_missing, :paginate }
end

My problem is, I only want to override the finders for some models. Currently I need to extend all association collection classes which are shared by all models. I know I can extend associations per model by passing a module:

has_many :things, :extend => SomeCustomMethods

But my library is basically am ActiveRecord plugin, so I'd like a clean convention for plugable finder extensions that apply to both the model and scoped collections without affecting all models in the application.

+3  A: 

First of all, make sure you know Ruby's method call inheritance structure well, as without this you can end up stabbing around in the dark.

The most straightforward way to do this inside an ActiveRecord class is:

def self.find(*args)
  super
end

That will apply to associations as well since they use the base finder themselves. Now you just need to do your customization. The complexity of that can vary widely, and I don't know what you're doing so I can't offer any recommendations.

Also defining this dynamically will be an exercise unto itself, but this should get you pointed in the right direction.

dasil003
+3  A: 

You want to override find_every, which is the AR method that will ultimately run find_by_sql with the corresponding query. Overriding find won't work for customized finders, and it's just more fragile.

But to be compatible with other plugins you can't just overload this method. Instead, alias it and call the original implementation after doing what you want:

module MyPlugin
  def self.included(base)
    class << base
      alias_method :find_every_without_my_plugin, :find_every
      def find_every(*args)
        # do whatever you need ...
        find_every_without_my_plugin(*args)
      end
    end
  end
end

ActiveRecord::Base.send :include, MyPlugin

This will enable your plugin for all classes. How do you want to control which models are enabled? Maybe a standard plugin accessor?

class User < ActiveRecord::Base
  my_plugin
end

To support this you need to move the class << base to a class method (thus base should be self). Like:

module MyPlugin
  def self.included(base)
    class << base
      base.extend ClassMethods
    end
  end

  module ClassMethods
    def my_plugin
      class << self
        alias_method :find_every_without_my_plugin, :find_every
        # ...
      end
    end
  end
end
Pedro
A: 

'Pedro's answer is right, but there's a small mistake.

def self.included(base)
  class << base
    base.extend ClassMethods
  end
end

should be

def self.included(base)
  base.extend ClassMethods
end

Using class << base ... end has the effect of calling 'extend' on 'base' in the class-method scope, but there is no method 'base' in ActiveRecord::Base so an error is thrown. Using base.extend by itself will call the 'extend' method of ActiveRecord::Base.

Casey