ActiveRecord中的Ruby集合类型

10
如果我在ActiveRecord中有一个带有子对象集合的对象,即:
class Foo < ActiveRecord::Base
  has_many :bars, ...
end

我尝试使用数组的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个回答

9
我假设这是因为ActiveRecord已经劫持了find方法用于自己的目的。
那不是真正的解释。foo_instance.bars不会返回Array实例,而是一个ActiveRecord::Associations::AssociationProxy实例。这是一个特殊的类,旨在充当持有关联的对象和关联对象之间的代理。
AssociationProxy对象充当数组,但它并不是真正的数组。以下细节直接摘自文档。
# 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的实例上调用。
foo_instance.bars.all.find { ... }

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


1
在这里澄清一下,.all方法实际上检索所有相关的模型,这取决于关联类型可能会对内存产生巨大的影响。例如,如果是User has_many:posts,则可能会检索用户的整个发布历史记录,这可能是大量数据。在可能的情况下,请尝试使用条件或命名范围构建查找调用以获得更好的性能。 - tadman

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#find方法,可以采取以下措施:
class << foo_instance.bars
  undef find
end
foo_instance.bars.find {...} # Array#find is now called

这是因为AssociationProxy类通过method_missing将所有未知的方法委托给其#target,即实际的底层数组实例。

3
ActiveRecord关联实际上是Reflection的实例,它重写了instance_of?和相关方法来欺骗它所属的类。这就是为什么你可以像添加命名作用域(比如“recent”)一样做事情,然后调用foo_instance.bars.recent。如果“bars”是一个数组,这将非常棘手。
尝试查看源代码(在任何类Unix-ish盒子上都可以跟踪到“locate reflections.rb”)。Chad Fowler已经就这个话题进行了非常有信息量的演讲,但我似乎找不到任何链接放在网上。(有人想编辑此帖以包含一些吗?)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接