ActiveRecord中的Ruby类型的集合

时间:2009-07-08 17:43:10

标签: ruby activerecord

如果我在ActiveRecord中有一个带有子对象集合的对象,即

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

我尝试对该集合运行Array的find方法:

foo_instance.bars.find { ... }

我收到:

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

我认为这是因为ActiveRecord为了自己的目的而劫持了find方法。现在,我可以使用detect,一切都很好。然而,为了满足我自己的好奇心,我试图使用元编程来明确地窃取find方法进行一次运行:

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

我收到此错误:

TypeError: bind argument must be an instance of Array

很明显Ruby不认为foo_instance.bars是一个数组而且:

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

有人可以帮我解释一下这个以及通过元编程来解决它的方法吗?

3 个答案:

答案 0 :(得分:9)

  

我认为这是因为ActiveRecord   已经劫持了它的find方法   自己的目的。

这不是真正的解释。 foo_instance.bars不返回Array的实例,而是返回ActiveRecord::Associations::AssociationProxy的实例。这是一个特殊类,旨在充当持有关联的对象和关联对象之间的代理。

AssociatioProxy对象充当数组,但它实际上不是数组。以下详细信息直接来自文档。

# 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.

如果您想处理结果数组,则根本不需要元编程技能。只需进行查询并确保在实际的Array对象上调用find方法,而不是在quacks like an array的实例上调用。

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

all方法是ActiveRecord finder方法(find(:all)的快捷方式)。它返回array个结果。然后,您可以在数组实例上调用Array#find方法。

答案 1 :(得分:6)

正如其他人所说,关联对象实际上并不是一个数组。要找出真正的类,请在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]

要删除在执行foo_instance.bars.find()时调用的ActiveRecord :: Bse #ref方法,以下内容将有所帮助:

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

这是因为AssociationProxy类将它不知道的所有方法(通过method_missing)委托给它的#target,它是实际的底层数组实例。

答案 2 :(得分:3)

ActiveRecord关联实际上是Reflection的实例,它会覆盖instance_of?和相关的方法来说谎它是什么类。这就是为什么你可以做一些事情,比如添加命名范围(比如说“recent”),然后调用foo_instance.bars.recent。如果“bars”是一个数组,这将非常棘手。

尝试检查源代码(“locate reflections.rb”应该在任何unix-ish框中跟踪它)。 Chad Fowler就这一主题发表了非常丰富的演讲,但我似乎无法在网上找到任何链接。 (任何人都想编辑这篇文章以包含一些内容吗?)