2009-07-08 11 views
9

Se ho un oggetto con una collezione di oggetti figlio in ActiveRecord, cioètipi di Ruby di collezioni in ActiveRecord

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

e si tenta di eseguire il metodo di Array find contro quella collezione:

foo_instance.bars.find { ... } 

Ricevo:

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

Presumo che ciò sia dovuto al fatto che ActiveRecord ha dirottato loMetodoper i propri scopi. Ora, posso usare detect e tutto va bene. Tuttavia, per soddisfare la mia curiosità, ho cercato di usare metaprogrammazione per rubare esplicitamente il metodo find posteriore per una corsa:

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

e ricevo questo errore:

TypeError: bind argument must be an instance of Array 

così chiaramente di Ruby non pensa foo_instance.bars è un array e tuttavia:

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

qualcuno può aiutarmi con una spiegazione di questo e di un modo per aggirare l'ostacolo con metaprogramm ing?

risposta

6

Come altri hanno già detto, un oggetto di associazione non è in realtà una matrice.Per scoprire la vera classe, farlo 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] 

per sbarazzarsi del ActiveRecord :: metodo # find Bse che viene chiamato quando si fa foo_instance.bars.find(), la seguente vi aiuterà:

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

Questo perché i delegati classe AssociationProxy tutti i metodi che non sa circa (via method_missing) alla sua #target, che è l'attuale grado matrice sottostante.

9

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

Questa non è la vera spiegazione. foo_instance.bars non restituisce un'istanza di Array ma un'istanza di ActiveRecord::Associations::AssociationProxy. Questa è una classe speciale intesa a fungere da proxy tra l'oggetto che detiene l'associazione e quello associato.

L'oggetto AssociatioProxy funge da matrice ma non è realmente una matrice. I seguenti dettagli sono presi direttamente dalla documentazione.

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

Se si vuole lavorare sulla serie di risultati, non hanno bisogno di competenze di metaprogrammazione a tutti. Basta fare la query e assicurarsi di chiamare il metodo find su un oggetto Array reale e non su un'istanza che sia quacks like an array.

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

Il metodo all è un metodo finder ActiveRecord (una scorciatoia per find (: all)). Restituisce uno array di risultati. Quindi è possibile chiamare il metodo Array#find sull'istanza dell'array.

+1

Per chiarire qui, il metodo .all in realtà recupera tutti i modelli associati che possono avere un enorme impatto sulla memoria a seconda del tipo di associazione. Ad esempio, se fosse Utente ha_molti: post, potresti recuperare l'intera cronologia di registrazione di un utente che potrebbe essere una notevole quantità di dati. Ove possibile, provare e costruire una chiamata di ricerca con condizioni o ambiti nominati per prestazioni migliori. – tadman

3

Le associazioni ActiveRecord sono in realtà delle istanze di Reflection, che sovrascrive instance_of? e metodi correlati per mentire su quale classe sia. Questo è il motivo per cui puoi fare cose come aggiungere scope nominati (diciamo "recenti") e poi chiamare foo_instance.bars.recent. Se "barre" fosse una matrice, sarebbe stato piuttosto complicato.

Prova a controllare il codice sorgente ("locate reflections.rb" dovrebbe tracciarlo su qualsiasi box unix-ish). Chad Fowler ha tenuto un discorso molto informativo su questo argomento, ma non riesco a trovare alcun collegamento in rete. (Qualcuno vuole modificare questo post per includerne alcuni?)

Problemi correlati