2009-07-08 4 views
9

Wenn ich ein Objekt mit einer Sammlung von untergeordneten Objekten in Activerecord, das heißtRuby-Typen von Sammlungen in Active

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

und ich versuche, Array des find Verfahren gegen diese Sammlung auszuführen:

foo_instance.bars.find { ... } 

Ich erhalte:

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

Ich nehme an, dies liegt daran, dass ActiveRecord dieentführt hatMethode für eigene Zwecke. Jetzt kann ich detect verwenden und alles ist in Ordnung. Aber meine eigene Neugier zu befriedigen, ich versuchte metaprogramming zu verwenden, um explizit die find Methode für einen Lauf zu stehlen zurück:

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

und erhalte ich diesen Fehler:

TypeError: bind argument must be an instance of Array 

so klar ist Rubin nicht denken foo_instance.bars ist ein Array und doch:

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

mir jemand mit einer Erklärung dafür helfen kann und einen Weg um ihn herum mit metaprogramm zu bekommen ing?

Antwort

6

Wie andere gesagt haben, ist ein Assoziationsobjekt nicht tatsächlich ein Array.Um die wirkliche Klasse herauszufinden, tun dies 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] 

Um loszuwerden der Active :: Bse # find Methode, die aufgerufen wird, wenn Sie tun foo_instance.bars.find(), wird folgendes helfen:

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

Dies liegt daran, dass die AssociationProxy Klasse delegiert alle Methoden es über (via method_missing) nicht weiß, seine #target, die die eigentliche zugrunde liegende Array-Instanz ist.

9

I assume this is because ActiveRecord has hijacked the find method for its own purposes.

Das ist nicht wirklich die wahre Erklärung. foo_instance.bars gibt keine Instanz von Array zurück, sondern eine Instanz von ActiveRecord::Associations::AssociationProxy. Dies ist eine spezielle Klasse, die als Proxy zwischen dem Objekt, das die Assoziation hält, und dem assoziierten Objekt dienen soll.

Das AssociatioProxy-Objekt fungiert als ein Array, aber es ist nicht wirklich ein Array. Die folgenden Details werden direkt aus der Dokumentation entnommen.

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

Wenn Sie an der Reihe von Ergebnissen arbeiten möchten, brauchen Sie überhaupt keine Metaprogrammierungsfähigkeiten. Machen Sie einfach die Abfrage und stellen Sie sicher, dass Sie die find-Methode für ein reales Array-Objekt aufrufen und nicht für eine Instanz, die quacks like an array ist.

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

all Die Methode ist eine Methode Active Finder (eine Abkürzung für find (: all)). Es gibt einen array von Ergebnissen zurück. Dann können Sie die Methode Array#find für die Array-Instanz aufrufen.

+1

Um hier zu verdeutlichen, ruft die .all-Methode tatsächlich alle zugehörigen Modelle ab, die je nach Art der Assoziation einen großen Speichereffekt haben können. Wenn es sich beispielsweise um Benutzer has_many: posts handelt, rufen Sie möglicherweise die gesamte Buchungshistorie eines Benutzers ab, bei der es sich um eine erhebliche Menge an Daten handeln könnte. Versuchen Sie nach Möglichkeit, einen Suchaufruf mit Bedingungen oder benannten Bereichen für eine bessere Leistung zu erstellen. – tadman

3

ActiveRecord-Zuordnungen sind eigentlich Instanzen von Reflection, die instance_of überschreiben? und verwandte Methoden, um darüber zu lügen, um was für eine Klasse es sich handelt. Aus diesem Grund können Sie z. B. benannte Bereiche hinzufügen (z. B. "recent") und dann foo_instance.bars.recent aufrufen. Wenn "Bars" ein Array wäre, wäre das ziemlich schwierig.

Versuchen Sie, den Quellcode zu überprüfen ("locate reflections.rb" sollte es auf einer beliebigen Unix-Box nachverfolgen). Chad Fowler hat einen sehr informativen Vortrag zu diesem Thema gehalten, aber ich kann im Internet keine Links finden. (Jeder möchte diesen Beitrag bearbeiten, um einige zu enthalten?)