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?
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With