Типы коллекций Ruby в 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

так ясно, что Руби не думает foo_instance.bars является массивом и все же:

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

3 ответов


как говорили другие, объект ассоциации на самом деле не является массивом. Чтобы узнать реальный класс, сделайте это в 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]

чтобы избавиться от метода ActiveRecord:: Bse#find, который вызывается при выполнении foo_instance.палка.найти (), поможет следующее:

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

это связано с тем, что класс AssociationProxy делегирует все методы, о которых он не знает (через method_missing) своему #target, который является фактическим базовым экземпляром массива.


Я предполагаю, что это потому, что ActiveRecord захватил метод find для его собственная цель.

это не совсем реальное объяснение. foo_instance.bars возвращает не экземпляр массива, а экземпляр 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.

если вы хотите работать на такие результаты, вам не нужны навыки метапрограммирования вообще. Просто сделайте запрос и убедитесь, что вызовите метод find для реального объекта массива, а не для экземпляра, который крякает как массив.

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

The all метод является методом поиска ActiveRecord (ярлык для find (: all)). Он возвращает array результатов. Тогда вы можете вызвать Array#find метод на массиве пример.


Ассоциации ActiveRecord на самом деле являются экземплярами отражения, которое переопределяет instance_of? и связанные с этим методы лгать о том, какой это класс. Вот почему вы можете делать такие вещи, как добавление именованных областей (скажем, "недавние"), а затем вызывать foo_instance.палка.недавний. Если бы "бары" были массивом, это было бы довольно сложно.

попробуйте проверить исходный код ("найдите отражения.rb " должен отслеживать его на любом Unix-ish box). Чад Фаулер дал очень информативный доклад об этом тема, но я не могу найти никаких ссылок на нее в интернете. (Кто-нибудь хочет отредактировать этот пост, чтобы включить некоторые?)