Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Ruby types of collections in ActiveRecord

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?

like image 676
kmorris511 Avatar asked Jul 08 '09 17:07

kmorris511


2 Answers

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.

like image 111
Simone Carletti Avatar answered Oct 13 '22 01:10

Simone Carletti


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.

like image 35
gerrit Avatar answered Oct 13 '22 00:10

gerrit