Finder a’la Ruby
Podczas pubowych gadek po ostatnim spotkaniu railsowców pojawił się zarzut, że findery w Railsach bardzo zalatują SQLem... Patrząc obiektywnie :) na tą część railsowego ORMa ciężko się nie zgodzić, właściwe poza dwoma pomysłami (mam na myśli with_scope i dynamiczne findery) reszta jest rzeczywiście niskopoziomowa.
Obiecałem swemu interlokutorowi, iż przyjrzę się sprawie...
Rozpoznanie rozpocząłem od zorientowania się czy nie próbuje wyważać otwartych drzwi i ... natrafiłem na ciekawy projekt o nazwie Mongoose (tak, tak autor czytał w młodości Kiplinga). Jest to prosty system zapisywania stanu obiektów z API wzorowanym nieco na ActiveRecord, zasadnicza różnica polega na tym ze Mongoose nie korzysta z relacyjnej bazy danych ale z systemu plików.
Zainspirowany sposobem tworzenia zapytań w Mongoose zaprojektowałem dla ActiveRecord sposób definiowania zapytań który wydaję się być już całkiem odległy od SQLa. Przy okazji wpadłem na pomysł jak rozwiązać kilka słabości jakie IMHO zawierają standardowe findery w Railsach, ale ad rem - oto kilka przykładów:
-
# znajdz użytkowników o nicku ‘daniel’ którzy
-
# się jeszcze nie logowali (licznik logowan jest zero)
-
User.finder do |user|
-
user.nick == 'daniel'
-
user.logins_count == 0
-
end
to samo jako SQL:
-
SELECT * FROM users WHERE nick = ‘daniel’ AND logins_count=0
i w standardowy sposób:
-
User.find_all_by_nick_and_logins_count('daniel', 0)
Czuć moc? Oczywiście... nie bardzo, zamiast jednej linijki wyszły cztery... to teraz coś większego:
-
# znajdz użytkowników którzy się nie logowali
-
# i są administratorami lub mają nick ‘daniel’
-
User.finder do |user|
-
user.logins_count == 0
-
user.any do
-
user.nick == 'daniel'
-
user.admin == true
-
end
-
end
wersja w SQL:
-
SELECT *
-
FROM users
-
WHERE (logins_count = 0) AND (nick = 'daniel' OR admin = true)
teraz chyba trochę lepiej :) a przecież można jeszcze tak:
-
User.finder do |user|
-
user.active == true
-
user.logins_count == [10, 20, 50]
-
user.any do
-
user.phone == /^0600/
-
user.website == nil
-
user.age.between(20, 35)
-
end
-
end
co daje SQL wyglądającego już troche straszniej:
-
SELECT *
-
FROM users
-
WHERE
-
(
-
active = true
-
AND
-
logins_count IN ('10', '20', '50')
-
) AND (
-
phone ~ '^0600'
-
OR
-
website IS NULL
-
OR
-
age BETWEEN 20 AND 35
-
)
Na zakończenie musze wspomnieć, że obcena implementacja jest w stanie dalekim od akceptowalnej jakości. Jeżeli więc byłby ktoś chętny do pomocy przy rozwinięciu przedstawionej tu koncepcji np. do postaci plugina o produkcyjnej jakości proszę o sygnał.
Lista niektórych braków:
- obsługa daty/czasu
- ujednolicenie API może zamiast
==używaćeq()(oraz obsługę negacji) - obsługa relacji (o ile w ogóle)
- adaptery do innych dialektów SQL niż PostgresQL
Źródełko jest tutaj.
[orm rails ruby]








Adam Hoscilo said,
sierpień 21, 2006 @ 12:04
To zly czlowiek musial byc, ktory zjechal Active Record :)
Tak serio to Danielu wiadomo, ze wiele da sie zrobic i wszystko bedzie dzialalo lepiej. Problem pojawia sie gdy np kod z nadbudowami ma przejsc pod czyjas opieke albo zmienia sie wersja Railsow i gdzies powstaja zgrzyty. Poki co chyba nie zanosi sie aby DHH myslal o przebudowaniu AR.
W Djangowym ORM wyglada to tak: http://www.djangoproject.com/documentation/models/or_lookups/
Adam Hoscilo said,
sierpień 21, 2006 @ 12:48
Dodatkowym atutem ORMa z Dajngo jest to, ze w tym przypadku mozesz zadac takie pytanie i jakos wynik dostajesz QuerySet - tj obiekt, na ktorym mozesz dalej zadawac pytania lub oczywiscie wyciagac obiekty, ktore ten queryset zawiera.
Wyglada to tak:
users_queryset = User.objects.filter(Q(logins_count=0) &
(Q(nick__contains”adam”) | Q(admin=True)))
ordered_users = users_queryset.order_by(”-last_login”)
filtered_users = users_queryset.filter(last_name__startswith=”Ho”)
#tu juz dostajemy obiekt
adamh = filtered_users.get(nick=”adamh”)
# mozemy leciec po QuerySecie
for user in ordered_users:
print “Imie: %s Nazwisko: %s, nick: %s” % (user.first_name, user.last_name, user.nick)
daniel said,
sierpień 21, 2006 @ 12:54
Moją prawdziwą intencją było raczej przyjrzenie się jak ciężkim zadaniem jest dodanie bardziej abstrakcyjnej warstwy do tworzenia zapytań - efekt zaskoczył mnie samego.
Kodzik który realizuje tą abstrakcje jest całkowicie niezależny od AR (a precyzyjniej zależy od rzeczy ktore sie raczej nie zmienią - find_by_sql i table_name pozostanie myśle zawsze). Dzięki możliwości przerobienia tego kodu w plugin, pozsotanie praktycznie zawsze niezależny od zmian w ActiveRecord.
Adam Hoscilo said,
sierpień 21, 2006 @ 13:58
Przyznaje, ze api wyglada blyskotliwie i pomysl jest dobry tyle, ze niestety niewiele to zmienia poki DHH nie zainteresuje sie tym by do AR wprowadzic abstrakcje.
PS Kod zrodlowy wyglada niezle - moze przydaloby sie udostepnic go swiatu nie tylko Polsce? Moze ktos zainteresowalby sie tematem.
Darek Rusin said,
wrzesień 22, 2006 @ 11:14
Hej Daniel,
Nie jestem biegły w temacie, ale nie jest to przypadkiem podobne do tego co robi Ezra w pluginie ez_where? Zerknij sobie na te linki:
http://brainspl.at/articles/2006/01/30/i-have-been-busy
http://brainspl.at/articles/2006/06/30/new-release-of-ez_where-plugin
daniel said,
wrzesień 22, 2006 @ 11:25
Witaj,
TAK TAK po trzykroć TAK, od kilku(nastu) dni używam właśnie ez_where, nie miałem tylko czasu, żeby go tu opisać ale już prawie prawie :)
Jarmark.org » Rails 2.0 okiem zgreda said,
październik 11, 2007 @ 12:10
[…] dodanie abstrakcji do tworzonych zapytań - sam robiłem małą przymiarkę do tego problemu, później odkryłem ez_where, a ostatnio pojawił się jeszcze Ambition […]