Question

In a Rails 3.2 model, was hoping to create a "to_csv" class method to generate CSV output from an ActiveRecord scope, like this:

class Post
   # ...
   def self.to_csv(options = {})
     CSV.generate(options) do |csv|
       scoped.each {|post| csv << record.attributes.values_at('title', 'text')}
     end
   end
end

I expected I could use it like this:

User.first.posts.to_csv

However, the method seems to be overridden by the Array #to_csv method. Even if I do this:

User.first.posts.scoped.to_csv

...and the result from User.first.posts.scoped is explicitly an ActiveRecord::Relation, I still hit the Array method. If I rename the method to something like "to_csvx" it works as expected.

I am hoping someone can explain how/why the ActiveRecord::Relation object preferentially hits the Array #to_csv method instead of the to_csv class method. While this (a class method expecting to be called by an AR scope) seems plausible, I am wondering if there's an inherent problem with the whole idea?

Was it helpful?

Solution

OK, super-lame for answering my own question, but based on comment from @m_x and Railscast 239 http://railscasts.com/episodes/239-activerecord-relation-walkthrough I deeper into the Rails source code... I can see that Relation respond_to honors, and method_missing prefers an Array method to any other method not explicitly defined for Relation.

# from rails/active_record/relation/delegation.rb

def respond_to?(method, include_private = false)
  super || Array.method_defined?(method) ||
    @klass.respond_to?(method, include_private) ||
    arel.respond_to?(method, include_private)
end

protected

def method_missing(method, *args, &block)
  if Array.method_defined?(method)
    ::ActiveRecord::Delegation.delegate method, :to => :to_a
    to_a.send(method, *args, &block)
  elsif @klass.respond_to?(method)
    ::ActiveRecord::Delegation.delegate_to_scoped_klass(method)
    scoping { @klass.send(method, *args, &block) }
  elsif arel.respond_to?(method)
    ::ActiveRecord::Delegation.delegate method, :to => :arel
    arel.send(method, *args, &block)
  else
    super
  end
end

So, if you name an ActiveRecord "scoping" class method the same as an Array instance method, the Relation will be converted to an Array and the Array method will be called instead of your class method.

Easy solution: just pick another method name.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top