Jako że ostatnie kilka tekstów było natury dywagacyjnej, a Ruby on Rails było poruszone tylko pobieżnie i niejako powierzchownie, tym razem będzie bardziej “technicznie”.
Nie czytam literatury dotyczącej RoR w języku innym, niż angielski, więc większość terminów będzie po angielsku, co mam nadzieję nie obrazi żadnych purystów językowych. Jako że nie znalazłem polskiego odpowiednika “eager loading“, będę się więc posługiwał taką właśnie formą. No chyba że bym się trochę wysilił z kreatywnością i przynajmniej spróbował to przetłumaczyć.
Może więc “chętne ładowanie”?
Albo - co brzmi dużo bardziej interesująco i ekstrawagancko - “skwapliwe ładowanie“?
Czym jest więc skwapliwe ładowanie? (Spodobał mi się ten termin, więc będę go stosował naprzemiennie z jego anglojęzycznym odpowiednikiem).
Otóż Rails daje nam proste i skuteczne narzędzia do tworzenia aplikacji internetowych. Jednakże prostota i skuteczność często pociąga za sobą mniejszą skłonność do myślenia i wręcz lenistwo.
Może więc na początek szybki quiz w stylu 1 z 10, kiedy na odpowiedź ma się około 5 sekund.
Sznuk: Kiedy stosuje się eager loading?
Programista RoR: Eager loading jest stosowany kiedy mamy do czynienia z powiązanymi modelami.
Sznuk: Do czego służy eager loading?
Programista RoR: Służy do zmniejszenia liczby zapytań do bazy danych i tym samym do usprawnienia i przyśpieszenia aplikacji. [ding!] Przynajmniej w większości przypadków. (Ostatnie zdanie, mimo iż prawdziwe, było już po “ding!”, sygnalizującym koniec czasu na odpowiedź, jednakże odpowiedź zaliczona).
Załóżmy więc, że mamy restaurację i w niej kilku dobrych kucharzy, którzy robą świetne dania i używają do tego swoich ulubionych składników
class Cook has_many :dishes has_many :ingredients, :through => :dishes class Dish belongs_to :cook has_many :ingredients class Ingredient belongs_to :dish
Jeśli więc chcielibyśmy sobie sprawdzić jakich to składników używa każdy z kucharzy (i dlaczego schodzi tak dużo rumu mimo iż nie jest składnikiem prawie żadnej potrawy), to będąc leniwymi programistami możemy sobie upichcić takie oto zapytanko:
@cooks = Cook.find(:all)
Później w widoku (view) pokażemy te składniki dla każdego z kucharzy:
<% for cook in @cooks %>
<%= cook.name %>:<br />
<% cook.ingredients.each do |i| %>
<%= i.name %>
<% end %>
<% end %>
I nic nie będzie w tym złego. Bo w końcu mamy tylko jedną restaurację, więc najwyżej kilku kucharzy. Nic więc strasznego, że dla każdej iteracji będzie generowane nowe zapytanie do bazy, żeby wydobyć listę składników. Jednakże jeśli byśmy mieli sieć restauracji w całej Europie i chcielibyśmy wiedzieć który kucharz spośród nich wszystkich używa więcej chili, niż przeciętny kucharz mąki, to już mielibyśmy problem, bo przy tak dużej ilości kucharzy nasza baza danych nie byłaby tak szybka w zaserwowaniu nam pysznych wyników.
Jednakże skwapliwe ładowanie stoi tuż za rogiem, machając nieśmiało ręką i próbując zwrócić na siebie uwagę. Przywołujemy więc je, pokazujemy mielącą dyskami i RAMem bazę danych i mówimy: “Zrób z tym coś”. Skwapliwe zabrawszy się za robotę, eager loading w końcu daje nam coś tak idealnie prostego, aczkolwiek skutecznego:
@cooks = Cook.find(:all, :include => :ingredients) @cooks = Cook.find(:all, :include => [:dishes, :ingredients])
Skutkuje to tym, że do bazy danych zostaje wystosowane jedno jedyne, długie jak spagetti zapytanie, które wszystkie potrzebne nam dane wyciąga za jednym zamachem. Przykładowe zapytanie (z kategorii innej niż kuchenna, ale żeby zobrazować długość możliwego zapytania) może być np. takie jak poniżej.
Jest to zapytanie wygenerowane przez ten niewinny kod:
@user_categories = current_user.categories.find(:all,
:include => [ :children => [ :children, :parent ]], :order => "categories.name")
SELECT `categories`.`id` AS t0_r0, `categories`.`cat_type` AS t0_r1, `categories`.`name` AS t0_r2, `categories`.`description` AS t0_r3, `categories`.`created_at` AS t0_r4, `categories`.`updated_at` AS t0_r5, `categories`.`parent_id` AS t0_r6, `categories`.`user_id` AS t0_r7, `childrens_categories`.`id` AS t1_r0, `childrens_categories`.`cat_type` AS t1_r1, `childrens_categories`.`name` AS t1_r2, `childrens_categories`.`description` AS t1_r3, `childrens_categories`.`created_at` AS t1_r4, `childrens_categories`.`updated_at` AS t1_r5, `childrens_categories`.`parent_id` AS t1_r6, `childrens_categories`.`user_id` AS t1_r7, `childrens_categories_2`.`id` AS t2_r0, `childrens_categories_2`.`cat_type` AS t2_r1, `childrens_categories_2`.`name` AS t2_r2, `childrens_categories_2`.`description` AS t2_r3, `childrens_categories_2`.`created_at` AS t2_r4, `childrens_categories_2`.`updated_at` AS t2_r5, `childrens_categories_2`.`parent_id` AS t2_r6, `childrens_categories_2`.`user_id` AS t2_r7, `parents_categories`.`id` AS t3_r0, `parents_categories`.`cat_type` AS t3_r1, `parents_categories`.`name` AS t3_r2, `parents_categories`.`description` AS t3_r3, `parents_categories`.`created_at` AS t3_r4, `parents_categories`.`updated_at` AS t3_r5, `parents_categories`.`parent_id` AS t3_r6, `parents_categories`.`user_id` AS t3_r7 FROM `categories` LEFT OUTER JOIN `categories` childrens_categories ON childrens_categories.parent_id = categories.id LEFT OUTER JOIN `categories` childrens_categories_2 ON childrens_categories_2.parent_id = childrens_categories.id LEFT OUTER JOIN `categories` parents_categories ON `parents_categories`.id = `childrens_categories`.parent_id WHERE (categories.user_id = 3) ORDER BY categories.name
Przy większej ilości danych średnia poprawa wydajności (nie według żadnych benchmarków, a według moich własnych i często subiektywnych obserwacji) jest co najmniej 2-3-krotna. Iterować po takich wynikach możemy dokładnie tak samo jak w poprzednim przypadku, tyle że tym razem już nie będą wysyłane żadne dodatkowe zapytania do bazy.
Sznuk: Pytanie finałowe dla Pana: czego nie potrafi eager loading?
Programista RoR: Eager loading nie potrafi gotować, prać, robić masażu i wybierać tylko poszczególne pola poprzez :select, bo tak bardzo jest skwapliwy, że [ding!] ładuje wszystko…
Sznuk: To jest poprawna odpowiedź! Gratuluję Panu wygranej w naszym programie. Wygrał Pan darmowe skwapliwe ładowanie trzech ciężarówek w malowniczym porcie w Gdyni. Pozdrawiam i do zobaczenia!
Trochę odbiegając już od tematu, jednak potwierdzę, że w przypadku eager loading opcja :select jest stanowczo i ostentacyjnie ignorowana. Jeśli nam jednak zależy na tej opcji, to możemy zrezygnować z :include na rzecz :joins, uzyskując pożądany wynik, aczkolwiek w trochę mniej elegancki sposób.
@posts = Post.find(:all, :select => "posts.id, posts.title, posts.subject, users.username", :joins => "left outer join users on users.id = posts.user_id")
Nie bądźmy więc leniwi bardziej niż to konieczne lub zdrowe i używajmy częściej skwapliwego ładowania, a baza danych będzie nam za to wdzięczna i w przyszłości, jak będziemy bardzo potrzebować jej dobrej pracy, spojrzy na nas łaskawszym okiem, westchnie głęboko i się zabierze za ładowanie.
Czytałem ostatnio sporo branżowych blogów, przeglądałem odpowiednie fora, trafiłem na kilka artykułów w Newsweeku / Time / Polityce / Fortune, przejrzałem polskie poletko Web 2.0 i wpadłem na świetny pomysł!