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:

RUBY:
  1. # znajdz użytkowników o nicku ‘daniel’ którzy
  2. # się jeszcze nie logowali (licznik logowan jest zero)
  3. User.finder do |user|
  4.     user.nick == 'daniel'
  5.     user.logins_count == 0
  6. end

to samo jako SQL:

SQL:
  1. SELECT * FROM users WHERE nick = ‘daniel’ AND logins_count=0

i w standardowy sposób:

RUBY:
  1. 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:

RUBY:
  1. # znajdz użytkowników którzy się nie logowali
  2. # i są administratorami lub mają nick ‘daniel’
  3. User.finder do |user|
  4.     user.logins_count == 0
  5.     user.any do
  6.         user.nick == 'daniel'
  7.         user.admin == true
  8.     end
  9. end

wersja w SQL:

SQL:
  1. SELECT *
  2. FROM users
  3. WHERE (logins_count = 0) AND (nick = 'daniel'  OR admin = true)

teraz chyba trochę lepiej :) a przecież można jeszcze tak:

RUBY:
  1. User.finder do |user|
  2.     user.active == true
  3.     user.logins_count == [10, 20, 50]
  4.     user.any do
  5.         user.phone == /^0600/
  6.         user.website == nil
  7.         user.age.between(20, 35)
  8.     end
  9. end

co daje SQL wyglądającego już troche straszniej:

SQL:
  1. SELECT *
  2. FROM users
  3. WHERE
  4. (
  5.     active = true
  6.     AND
  7.     logins_count IN ('10', '20', '50')
  8. ) AND (
  9.      phone ~ '^0600'
  10.      OR
  11.      website IS NULL
  12.      OR
  13.      age BETWEEN 20 AND 35
  14. )

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.

[ ]
Spodobało się? Podziel się z innymi: These icons link to social bookmarking sites where readers can share and discover new web pages.
  • del.icio.us
  • Wykop
  • Gwar
  • Digg
  • Technorati

Liczba komentarzy: 7 »

  1. 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/

  2. 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)

  3. 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.

  4. 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.

  5. 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

  6. 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 :)

  7. 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 […]

RSS feed for comments on this post · Adres TrackBack

Dodaj komentarz