Eager load z poziomu obiektu

W Rails 3.2 dodałem sobie taką fajną metodę do ActiveRecord::Base:

def eager_load(*args)
  ActiveRecord::Associations::Preloader.new(self, *args).run
end

Pozwalało to na doładowanie asocjacji z poziomu obiektu bez przeładowania go z includem, np. tak:

u = User.first
# Coś innego...
u.eager_load(:characters) # SELECT `characters`.* FROM `characters` WHERE `characters`.`user_id` IN (?)

Niestety od Rails 4.1 preloader trochę się zmienił. Raz, że interfejs jest inny - teraz moja metoda wygląda tak:

def eager_load(*args)
  ActiveRecord::Associations::Preloader.new.preload(self, *args)
end

Ale i tak nie działa to poprawnie. Tu przykład, gdzie wywołanie eager_load powoduje dodatkowe dwa selecty na całej tabeli:

2.1.2 :001 > u = User.first
[2015-01-06 23:18:03] DEBUG ActiveRecord::Base :   User Load (0.3ms)  SELECT  `users`.* FROM `users`  ORDER BY `users`.`id` ASC LIMIT 1
 => #<User id: 1, ...> 
2.1.2 :002 > u.eager_load :characters
[2015-01-06 23:18:07] DEBUG ActiveRecord::Base :   Character Load (0.2ms)  SELECT `characters`.* FROM `characters` WHERE `characters`.`user_id` IN (1)
[2015-01-06 23:18:07] DEBUG ActiveRecord::Base :   Character Load (0.3ms)  SELECT `characters`.* FROM `characters`
[2015-01-06 23:18:07] DEBUG ActiveRecord::Base :   Character Load (0.2ms)  SELECT `characters`.* FROM `characters`
 => [#<ActiveRecord::Associations::Preloader::HasMany:0x00000007c26d28 @klass=Character(id: integer, ...), @owners=[#<User id: ...], @reflection=#<ActiveRecord::Reflection::HasManyReflection:0x0000000496aa60 @name=:characters, ...(b. dużo śmieci)...] 

To bug? Czy ja coś źle robię…?

PS. Sprawdzałem z różnymi wersjami Ruby i wszędzie to samo.

Podejrzewam że prostszą wersją było by:

def preload(:stuff)
  send(:stuff).to_a
end

.to_a wymusza załadowanie wszystkich obiektów.

Ale generalnie railsy dają możliwośc ładowania powiązanych obietków w API, poczytaj: http://blog.bigbinary.com/2013/07/01/preload-vs-eager-load-vs-joins-vs-includes.html (nie jestem pewien czy odn osi się ten post do Rails 4 ale można łatwo sprawdzić).

Niestety funkcje API (preload, includes, joins) działają tylko na klasie, a nie na obiekcie, tzn. można zrobić User.includes(...), ale już nie user.includes(...).

to_a jest jakimś rozwiązaniem, ale tylko do prostych asocjacji, a ja chciałbym zrobić to samo, co mogę wykonać w includes, np.:

user.eager_load(:posts, :characters => [:friends, :foes])

W ostateczności mogę przeładować cały obiekt, w końcu to tylko jedno dodatkowe zapytanie, ale jakoś nie chce mi się wierzyć, że inaczej się nie da…

Z czystej ciekawości po co ci eager loading na poziomie obiektu?
Po co wymuszać zapytania jeśli mogą się okazać niepotrzebne? Bo jakiejś oszczędności w ilości zapytań nie widzę zupełnie.
Cięzko mi sobie wyobrazić jakikolwiek scenariusz w którym taki preloading na poziomie obiektów byłby pożądany.

Przyda się wszędzie tam, gdy dopiero po spojrzeniu na obiekt dowiadujemy się, czy będziemy korzystać z asocjacji:

user = User.find(params[:user_id])
if user.active?
  user.eager_load :foos => [:boos, :moos]
  user.foos.each do |f| .....  # Coś z tym robimy
else
  ...  # Robimy coś innego
end

Nie odpowiedziałeś na jedną część pytania. Po co ładować je przed użyciem?
Serio czemu nie po prostu.

user = User.find(params[:user_id])
if user.active?
  user.foos.each do |f| 
    f.boos.each do |x|
      # itd.
    end
  end
else
  ...  # Robimy coś innego
end

Dokłądnie taka sama ilosć zapytań. A jak okaże sieże w zależności od innego warunku nie będziesz korzystał z moos to będzie nawet o zapytanie mniej.

PS. Jak mówię generalnie po kiego ch… ale jak się bardzo upierasz to zawsze możesz:

def preloaded_foos
  @foos ||= Foo.for_user(self).includes(:boos, :moos)
end
1 Like

Nie taka sama ilość zapytań, tylko N+1 zamiast 2, dla każdego foo zostaną załadowane jego boos. Dokładnie dlatego potrzebuję eager load.

Odnośnie preloaded_foos - czasami można i tak, aczkolwiek znowu pojawi się problem, jeśli foo odwołuje się do usera (kolejne zapytanie… dla każdego z foo).

@astaroth szukasz dziury w całym. Jak naprawdę musisz to robisz:

def preloaded_foos
 @foos ||= Foo.for_user(self).includes(:boos, :moos).map{|f| f.user = self}
end

Jeżeli naprawdę naprawdę masz use case’a dla tego potworka to używaj. Mi to jesdnak bardzo podejrzanie wygląda jak premature optiization.

Dodatkowo jakbym znał szczegóły to by mnajprawdopodobniej zaproponował coś sensowniejszego.

Powiem tak, ręczny preloading śmierdzi na kilometr