views:

481

answers:

3

If I have an object with a collection of child objects in ActiveRecord, i.e.

class Foo < ActiveRecord::Base
  has_many :bars, ...
end

and I attempt to run Array's find method against that collection:

foo_instance.bars.find { ... }

I receive:

ActiveRecord::RecordNotFound: Couldn't find Bar without an ID

I assume this is because ActiveRecord has hijacked the find method for its own purposes. Now, I can use detect and everything is fine. However to satisfy my own curiousity, I attempted to use metaprogramming to explicitly steal the find method back for one run:

unbound_method = [].method('find').unbind
unbound_method.bind(foo_instance.bars).call { ... }

and I receive this error:

TypeError: bind argument must be an instance of Array

so clearly Ruby doesn't think foo_instance.bars is an Array and yet:

foo_instance.bars.instance_of?(Array) -> true

Can anybody help me with an explanation of this and of a way to get around it with metaprogramming?

+6  A: 

I assume this is because ActiveRecord has hijacked the find method for its own purposes.

That's not really the real explanation. foo_instance.bars doesn't return an instance of Array but an instance of ActiveRecord::Associations::AssociationProxy. This is a special class intended to act as a proxy between the object that holds the association and the associated one.

The AssociatioProxy object acts as an array but it isn't really an array. The following details are taken directly from the documentation.

# Association proxies in Active Record are middlemen between the object that
# holds the association, known as the <tt>@owner</tt>, and the actual associated
# object, known as the <tt>@target</tt>. The kind of association any proxy is
# about is available in <tt>@reflection</tt>. That's an instance of the class
# ActiveRecord::Reflection::AssociationReflection.
#
# For example, given
#
#   class Blog < ActiveRecord::Base
#     has_many :posts
#   end
#
#   blog = Blog.find(:first)
#
# the association proxy in <tt>blog.posts</tt> has the object in +blog+ as
# <tt>@owner</tt>, the collection of its posts as <tt>@target</tt>, and
# the <tt>@reflection</tt> object represents a <tt>:has_many</tt> macro.
#
# This class has most of the basic instance methods removed, and delegates
# unknown methods to <tt>@target</tt> via <tt>method_missing</tt>. As a
# corner case, it even removes the +class+ method and that's why you get
#
#   blog.posts.class # => Array
#
# though the object behind <tt>blog.posts</tt> is not an Array, but an
# ActiveRecord::Associations::HasManyAssociation.
#
# The <tt>@target</tt> object is not \loaded until needed. For example,
#
#   blog.posts.count
#
# is computed directly through SQL and does not trigger by itself the
# instantiation of the actual post records.

If you want to work on the array of results, you don't need metaprogramming skills at all. Just make the query and make sure call the find method on a real Array object and not on an instance that quacks like an array.

foo_instance.bars.all.find { ... }

The all method is an ActiveRecord finder method (a shortcut for find(:all)). It returns an array of results. Then you can call the Array#find method on the array instance.

Simone Carletti
To clarify here, the .all method actually retrieves all the associated models which can have a huge memory impact depending on the type of association. For example, if it was User has_many :posts, you might be retrieving a user's entire posting history which could be a substantial amount of data. Where possible, try and construct a find call with conditions or named scopes for better performance.
tadman
+2  A: 

ActiveRecord associations are actually instances of Reflection, which overrides instance_of? and related methods to lie about what class it is. This is why you can do things like adding named scopes (say, "recent") and then calling foo_instance.bars.recent. If "bars" was an Array, this would be pretty tricky.

Try checking out the source code ("locate reflections.rb" should track it down on any unix-ish box). Chad Fowler has given a very informative talk on this topic, but I can't seem to find any links to it out on the web. (Anyone want to edit this post to include some?)

John Hyland
+3  A: 

As others have said, an association object isn't actually an Array. To find out the real class, do this in irb:

class << foo_instance.bars
  self
end
# => #<Class:#<ActiveRecord::Associations::HasManyAssociation:0x1704684>>

ActiveRecord::Associations::HasManyAssociation.ancestors
# => [ActiveRecord::Associations::HasManyAssociation, ActiveRecord::Associations::AssociationCollection, ActiveRecord::Associations::AssociationProxy, Object, Kernel]

To get rid of the ActiveRecord::Bse#find method that gets called when you do foo_instance.bars.find(), the following will help:

class << foo_instance.bars
  undef find
end
foo_instance.bars.find {...} # Array#find is now called

This is because the AssociationProxy class delegates all methods it doesnt know about (via method_missing) to its #target, which is the actual underlying array instance.

gerrit