Entries from Czerwiec 2008 ↓

Ruby on Rails :include i ‘eager loading’ w akcji

eager loadingJako ż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.

Mam pomysł, potrzebuję tylko programisty

Czy tego szukam? 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ł!
Teraz potrzebuję tylko programisty, który mi to szybciutko skleci i będę kolejnym złotym dzieckiem polskiego Internetu. Na szczęście w Polsce takowych jest jeszcze bardzo mało, więc tym bardziej mam spore szanse.

Taki mniej więcej tok zdarzeń i rozumowania jest aż nazbyt częsty. Zarówno w Polsce, jak i w innych krajach, ale na innych się skupiać nie będę, więc będę pisał o rodzimym rynku.

Jest niestety bardzo dużo przypadków jak powyżej i odpowiednich ogłoszeń "szukam programisty". Przy czym dalszy tok rozumowania wygląda następująco:
- Zrób mi proszę prosty serwis (a i tak dobrze, jeśli pada słowo "proszę")
- taki coś w stylu Allegro / Nasza-Klasa / Web 2.0 / Flickr (w zależności na jaki genialny pomysł się wpadło)
- to przecież powinno być proste
- trochę PHP, trochę MySQL, oczywiście AJAX i powinno być gotowe (ilość i stopień wyszukania akronimów odpowiedni do stopnia oczytania branżowych blogów i znajomości aktualnych topowych buzzwordów)
- jako że to jest proste, to jestem nawet skłonny zapłacić 1000-2000 PLN (co też jest wspaniałomyślne, bo równie często prosi się znajomego programistę o sklecenie podobnego serwisu po znajomości)
- dodaj jeszcze do tego serwisu funkcje X, Y i Z i po sprawie (przy czym te właśnie funkcje, nawet jeśli są całkowicie pozbawione sensu z punktu widzenia przeznaczenia serwisu, powinny z niego zrobić co najmniej drugiego eBay’a, jak nie lepiej)

Kwestie, o których się nawet nie myślało
- godne wynagrodzenie za wykonanie pomysłu (to 1-2K nie jest godne?!)
- podział zysków z programistą / developerem
- udziały w przedsięwzięciu dla programisty / developera
- no i oczywiście najważniejsze – zasadność tworzenia takiego serwisu

Kwestie, o których w wielu wypadkach się nawet nie słyszało
- User Interface
- Pageflow
- Struktura bazy danych
- Bezpieczeństwo (serwisu, serwera, danych użytkowników)
- Skalowalność

Powyższe kwestie nie zostały wyssane z palca – widziałem zbyt wiele ogłoszeń typu "1-2K PLN za serwis o funkcjonalności Allegro", przy czym zawarte jest w tym wszystko od zarejestrowania domenu do uruchomienia serwisu. Zbyt wiele, żeby uznać to za jednostkowe przypadki.

Jeśli już nawet zasadne byłyby przypadki, kiedy tzw. "biz guy" szuka "tech guy" do zrobienia roboty, to rozumowanie powinno się zaczynać od końca:
- Jaka jest zasadność tworzenia takiego serwisu? (pytanie to powinno być zadane wielokrotnie i pod wieloma kątami, dopiero później można przechodzić do pytań kolejnych)
- jak będzie wyglądał / kto stworzy UI
- jaka będzie struktura serwisu (pageflow)
- jaki będzie podział ról między biz / tech guy
- jaki będzie podział zysków / udziałów między nimi
- jak znaleźć dobrego programistę / developera, który na dodatek będzie chciał pracować z kimś, kto ze strony technicznej nic nie wniesie do przedsięwzięcia (równie dobrze może to być na pierwszym miejscu, jeśli chodzi o wpływ, jaki ma to na powodzenie projektu)
- dopiero później cała reszta

Ciekawe dlaczego nie ma pytań ze strony programistów potrzebuję tylko człowieka do generowania pomysłów, a z resztą sobie poradzę, prawda?

Zawsze źle oceniamy to, czego nie rozumiemy. A to, co rozumiemy, możemy zrobić sami.
Dlatego też powinno być tak, że to developerzy powinni poszukiwać ludzi, którzy się zajmą "stroną biznesową" serwisu, który już stworzyli, a nie odwrotnie. Dlaczego tak nie jest? W dużej mierze przedstawiłem to tutaj: Dlaczego powstaje mało serwisów mimo ciągle zmniejszającej się bariery wejścia?

Moim zdaniem jeśli już, to właśnie w odwrotną stronę powinno to działać, czyli do gotowego serwisu powinna być poszukiwana osoba, która się zajmie żmudną pracą wprowadzania go na szerokie wody. zakładając oczywiście, że osoba, która go stworzyła, chce się zajmować tylko techniczną stroną i dlatego takiej osoby poszukuje. Tudzież z braku czasu na obie role naraz. Jednak takiego ogłoszenia, nawet jednego, jeszcze w Polsce nie widziałem…

Idealnym rozwiązaniem jednak jest współpraca dwóch takich osób od samego początku. Nie szukanie kogoś tylko do zrobienia czegoś, a wspólne i ukierunkowane działania od etapu generowania pomysłu, poprzez projektowanie interfejsu, poszukiwania potencjalnych źródeł przychodów, do wyprowadzania projektu na szerokie wody. Jako że jest to jednak sytuacja idealna i wymagająca całkowitego poświęcenia dwóch osób, co w Polsce jeszcze jest rzadko spotykane, poświęciłem temu zagadnieniu powyższy post.