Sequel vs Active Record. Część 1 - słabości AR.

Opublikowane przez Jarosław Zabiełło Wed, 12 Dec 2007 23:37:00 GMT

Gdy framework Ruby on Rails (dalej: RoR lub Rails) rozpoczynał swoją błyskotliwą karierę, zachwycał wszystkich prostotą i elegancją konstrukcji. Jednym z fundamentalnych modułów Railsów jest Active Record, biblioteka realizująca symulację wirtualnej bazy obiektowej, pozwalającą na pracę z relacyjnymi bazami danych za pomocą obiektów języka Ruby. Active Record (dalej AR) nie uniknął jednak kilku potknięć projektowych.

Brak parametryzowanych zapytań SQL

Kompletnie nie rozumiem po co Active Record męczy się z budowaniem całej kwerendy SQL. Po co też jego tworcy tracą czas na walkę z analizą i odpowiednim escape’owaniem znaków w danych przekazywanych do zapytania. Przecież sterownik MySQL do Rubiego bez problemu to wszystko zrobi na nich! Mało tego, używanie parametryzowanych zapytań SQL jest szybsze niż samodzielne budowanie całego stringa i wykonywanie na nim kwerendy.

Brak iteratorów

Inną tajemnicą developerów AR jest brak korzystania z iteratorów. Zamiast tego AR, z uporem godnym lepszej sprawy, przepycha przez pamięć całą listę obiektów Rubiego. Im pobieramy więcej rekordów z bazy tym więcej obiektów musimy przesłać do pamięci (bo każdemu rekordowi odpowiada jeden egzemplarz klasy Active Record). AR wrzuca więc wszystkie obiekty ORM do pamięci. Trochę to bez sensu, bo bazy danych są optymalizowane na pobieranie rekordów małymi porcjami. Grupowane pobieranie/wkładanie rekordów potrafi być nawet o dwa rzędy wielkości szybsze. Gdyby DHH i koledzy popatrzyli dokładniej na standard PEP249 Pythona a nie na chaotyczne praktyki rodem z PHP, to by takiego błędu nie popełnili.

Domyślne SELECT * i błąd w :include

Gdy AR wykonuje zapytanie komendę Model.find(:all) to domyślnie wykonuje “SELECT *...” Nie byłoby z tym problemu gdyby nie to, że MySQL nie potrafi indeksować pół typu TEXT/BLOB. Pobieranie “hurtem” wszystkich kolumn z których jedna jest tego typu, dramatycznie spowalnia bazę danych.

No dobrze, ktoś powie, że można zawsze użyć opcji :select i wybrać sobie pola jakie chcemy. Niestety to się sypie, gdy chcemy użyć opcji :include aby uzyskać LEFT OUTER JOIN. Użycie opcji :include anuluje bowiem efekt działania opcji :select. (Aby to obejść trzeba zastąpić opcję :include opcją :joins i wklepać ręcznie cały fragment SQL. Opcja :joins nie koliduje z :select).

Niespójność składni :include i :joins.

To może mała sprawa, ale razi. Dlaczego :include pozwala na stosowanie ładnego zapisy :include => [:tabela1, :tabela2] a :joins przyjmuje tylko wartość typu String, tj. czysty SQL?

Aktualizacja jednej kolumny to aktualizacja wszystkich kolumn rekordu.

To już jest bardziej denerwujące. Chcemy zmienić fragment rekordu a AR serwuje nam update na wszystkich kolumnach. Nie ma jak to obejść bez uciekania się do odpalenia czystego SQL’a. Całe szczęście że AR na to pozwala.

Brak obsługi złożonych kluczy obcych

AR domyślnie w ogóle nie obsługuje złożonych kluczy obcych (Sympatycy pythonowego Django nie mają tu też powodu do dumy, bo ich ORM również tego nie potrafi). Sytuację trochę ratuje gem/plugin composite_primary_keys ale jak to z pluginami w Rails bywa, im ich mniej tym lepiej. No chyba, że ktoś sam prosi się o kłopoty. Z jakością pluginów jest różnie i trzeba ostrożnie je dobierać.

Brak krokowego budowania warunków

To jest jedna z bardziej denerwujących rzeczy. AR gromadzi warunki w wartości do klucza/opcji :conditions. Może on przyjmować wartość typu String, Array lub Hash ale tylko jedną naraz i tylko raz. Jeśli chcemy zbudować jakiś bardziej złożony warunek, to musimy sklejać sobie ręcznie kwerendę i uważać aby sobie przypadkiem nie strzelić jakimś przypadkowym sql-injection w stopę.

Nie wiem czy jest nadzieja na szybką poprawę jakości implementacji Active Record, bo mam wrażenie że developerzy i kontrybutorzy skupieni wokół tego frameworka wolą gonić za nowymi funkcjami zamiast zatrzymać się i dopracować to, co już stworzono. Na szczęście nie trzeba czekać. Active Record doczekał się godnych następców. Najbardziej znaczące są dwa: DataMapper i Sequel. Szczególnie godny uwagi jest Sequel. Ale o tym w następnej części.

Zobacz Sequel vs Active Record. Część 2 – integracja Rails z Sequelem

Tagi , , ,  | 5 comments

Comments

  1. Avatar Paweł Kondzior powiedział about 10 hours later:

    Co racja to racja, Rails 2.0 powinno sie nazywa 1.4 conajwyzej, a same 2.0 powinno przedstawiac nowa jakosc ActiveRecord i zlamanie kompatybilnosci wstecznej, co by pozwolilo na lepsze dopracowanie technologii. DHH mysli w troche inny sposob i widac wyrazne na przykladach jego wypowiedzi na temat wielowatkowosci rails, Ezra troche utarl mu nosa Merbem :) i pokazal ze sie da zrobic prosty technicznie framework z wielowatkowoscia ktory jest szybszy od rails.

  2. Avatar Radarek powiedział about 11 hours later:

    Na pewno w dużej mierze masz rację, sam pod wieloma punktami się podpiszę (iteratory i parametryzowane zapytania powinny już dawno być).

    Jednakże chciałbym zwrócić na pewne aspekty takich a nie innych rozwiązać jakie zastosowano w AR.

    Kwestia domyślnego SELECT * i przy zapisie update KAŻDEJ kolumny. Ciężko tu powiedzieć, że to jest złe, ale wynika to z przyjętej taktyki. Otóż założenie jest takie, że obiekt (wiersz z tabeli) reprezentuje pewien stan (wartości atrybutów). Stan ten możesz walidować, zmieniać i zapisywać do bazy. Wyobraźmy sobie taką sytuację. Model A, dwa atrybuty x, y. Walidacja => suma x + y musi należeć do przedziału [0..10]. Problem zacznie się gdy dwaj niezależni użytkownicy zaczną zmieniać taki obiekt. Jeden zmieni wartość atrybutu x, drugi y, obiekty zostaną zwalidowane (poprawnie) i gdyby do bazy poszły dwa osobne zapytania: UPDATE A set x = ... UPDATE A set y = ... to może dojść do sytuacji, że nagle będziesz mieć w bazie obiekt, który nie spełnia założonej walidacji. Stąd takie rozwiązanie projektowe – ma to swój sens. Podobnie jest z wybieraniem wszystkim pól. Podczas walidacji nie zastanawiasz się, które pole zostało pobrane, tylko manipulujesz nim (sprawdzasz jego wartość itp). Gdyby dany atrybut mógł nie zostać pobrany to musiałbyś każdy taki przypadek uwzględnić. Musiałbyś także rozróżniać czy dany atrybut nie został po prostu podany (wartość nil) czy też nie został pobrany z bazy (jaka specjalna flaga itp). Na pewno doprowadziłoby to do pewnej komplikacji.

    Co do braku krokowego budowania warunków. I tak i nie. Przecież przesyłasz hash z warunkami, więc możesz sobie taki hash budować stopniowo.

    cond = {:conditions => “name = ‘matz’” } User.find(:all, cond)

    cond[:limit] = 2 User.find(:all, cond)

    Używałem kiedyś w php Propela, który zdaje się wzoruje się na Hibernate (jeśli się mylę to mnie poprawcie). Tam to dopiero była porażka bo budowało się mozolnie obiekt Criteria, ustawiało warunki (dodanie warunku war1 and (war2 or war3) wcale nie było takie proste i intuicyjne) i w końcu można było wykonać zapytanie. Co z tego, że miałem piękny obiekt gotowy do ponownego użycia, jak w praktyce był potrzebny tylko w tym jednym miejscu. Wolę prostotę Rubiego i oparcie pewnych operacji na typach prostych, wbudowanych w język – tu hash. Z tego powodu find(:all) zwraca tablicę, ale autorzy AR mogli tu spokojnie “za kulisami” zastosować iterator.

  3. Avatar rsz powiedział about 14 hours later:

    To merb jest wielowątkowy? Ach, ta dzisiejsza młodzież, tak się lubuje w redefiniowaniu starych, dobrych pojęć.

  4. Avatar Pijoter powiedział about 23 hours later:

    O grzechach AR można także poczytać pod adresem: http://mysqldump.azundris.com/archives/72-Rubyisms.html

    Warto zapoznać się z komentarzami niejakiego “jp”, który wyjaśnia dlaczego AR zachowuje się tak jak się zachowuje.

  5. Avatar qqrq powiedział 4 months later:

    “Używałem kiedyś w php Propela, który zdaje się wzoruje się na Hibernate (jeśli się mylę to mnie poprawcie)” – niby tak, a niby nie. Obiekt criteria z tego co wiem, jest nie bardzo Hibernate-owy – tam jest HQL (taki obiektowy SQL). W PHP jest za to Doctrine – tam jest DQL (PHP-owy odpowiednik HQL-a).

(leave url/email »)

   Pomoc języka formatowania Obejrzyj komentarz