2009-07-08 18 views
9

Jeśli mam obiektu z kolekcji obiektów podrzędnych w ActiveRecord, tjRuby rodzaje zbiorów w ActiveRecord

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

i próbie uruchomienia Array find metodę przeciwko tej kolekcji:

foo_instance.bars.find { ... } 

Otrzymuję:

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

Zakładam, że to dlatego, że ActiveRecord przejął find Metoda dla własnych celów. Teraz mogę użyć detect i wszystko jest w porządku. Jednak, aby zaspokoić własne ciekawość, próbowałem użyć metaprogramowanie jawnie ukraść metodę find powrotem na jednym cyklu:

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

i otrzymuję ten błąd:

TypeError: bind argument must be an instance of Array 

tak wyraźnie Ruby nie myśli foo_instance.bars jest tablicą i jeszcze:

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

Czy ktoś może mi pomóc z wyjaśnieniem tego i sposób poruszania się po niej z metaprogramm ing?

Odpowiedz

6

Jak powiedzieli inni, obiekt powiązania nie jest tak naprawdę tablicą.Aby dowiedzieć się prawdziwą klasę, to zrobić w 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] 

aby pozbyć się ActiveRecord :: BSE metodą # okaże się, że jest wywoływana gdy robisz foo_instance.bars.find() dodaje pomoże:

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

to dlatego, że klasa deleguje AssociationProxy wszystkie metody robi wiedzieć (przez method_missing) do jego #target, który jest rzeczywisty przypadek bazowy tablicy.

9

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

To naprawdę nie jest prawdziwe wytłumaczenie. foo_instance.bars nie zwraca instancji Array, ale instancji o numerze ActiveRecord::Associations::AssociationProxy. Jest to specjalna klasa przeznaczona do pełnienia funkcji pośrednika między obiektem, który przechowuje skojarzenie i powiązanym.

Obiekt AssociatioProxy działa jak tablica, ale tak naprawdę nie jest tablicą. Poniższe informacje pochodzą bezpośrednio z dokumentacji.

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

Jeśli chcesz pracować nad szeregiem wyników, w ogóle nie potrzebujesz umiejętności metaprogramowania. Po prostu wykonaj zapytanie i upewnij się, że wywołujesz metodę find na prawdziwym obiekcie Array, a nie na instancji, która jest quacks like an array.

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

all metoda to metoda finder ActiveRecord (skrót dla find (: all)). Zwraca on array wyników. Następnie możesz wywołać metodę Array#find na instancji tablicy.

+1

Aby to wyjaśnić, metoda .all faktycznie pobiera wszystkie skojarzone modele, które mogą mieć ogromny wpływ na pamięć w zależności od typu powiązania. Na przykład, jeśli był to użytkownik has_many: posty, możliwe, że pobierasz całą historię księgowania użytkownika, która może być znaczną ilością danych. Tam, gdzie to możliwe, spróbuj skonstruować wywołanie find z warunkami lub nazwanymi zakresami dla lepszej wydajności. – tadman

3

Stowarzyszenia ActiveRecord są w rzeczywistości instancjami Reflection, które zastępują instance_of? i pokrewnymi metodami kłamania o tym, jaka jest klasa. Dlatego możesz robić rzeczy takie jak dodawanie nazwanych zakresów (powiedzmy "ostatnie"), a następnie wywoływać foo_instance.bars.recent. Gdyby "bary" były tablicą, byłoby to dość trudne.

Spróbuj sprawdzić kod źródłowy ("zlokalizuj reflections.rb" powinien wyśledzić go w dowolnym polu unixowym). Chad Fowler przeprowadził bardzo pouczającą dyskusję na ten temat, ale nie mogę znaleźć żadnych linków do niego w Internecie. (Ktoś chce edytować ten post, aby dołączyć niektóre?)

Powiązane problemy