0

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?

Tom Wilson
  • 797
  • 9
  • 26
  • On the surface this seems similar to http://stackoverflow.com/questions/224128/how-do-you-make-a-method-apply-to-a-collection-of-activerecord-objects but the actual point of this question is how can I get the records I need to covert by chaining this on the end of a Relation chain instead of passing them in as an argument. – Tom Wilson Apr 15 '13 at 17:14
  • 1
    i'm just guessing here : your `Array#to_csv` method seems to come from a gem / monkey patch (AFAIK it is not from rails itself). Maybe this gem also monkey patches `Relation` in order to delegate `to_csv` to `to_a` (see [ActiveRecord::Delegation](https://github.com/rails/rails/blob/master/activerecord/lib/active_record/relation/delegation.rb) ). It would explain your problem, and why using another method name works. – m_x Apr 15 '13 at 17:47
  • I should have noted Ruby version (1.9.3) The Array to_csv method is shown here: http://apidock.com/ruby/Array/to_csv – Tom Wilson Apr 15 '13 at 18:15
  • my bad, i tried this in an old box of mine in ruby 1.8. I don't see why in hell such method is in ruby core ? – m_x Apr 16 '13 at 07:06

1 Answers1

4

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.

Tom Wilson
  • 797
  • 9
  • 26
  • 2
    It's not lame to answer your own question, in fact you should even accept it. Contributes to SO and helps people... nice digging into the source. I find it weird that array methods are preferred over relation methods, maybe you should suggest a patch to rails on github – m_x Apr 16 '13 at 07:04