Elastyczność Railsów

Opublikowane przez Jarosław Zabiełło Wed, 04 Oct 2006 15:01:00 GMT

Większość osób, które zetknęły się z frameworkiem Ruby on Rails zauważyła, że domyślne ustawienia są tam tak dobrane, aby maksymalnie uprościć i zminimalizować ilość ręcznej pracy przy ustawieniach. Jeśli więc ktoś trzyma się flozofii i konwencji nazw (np. dla tabel i ich pól) będzie miał znacznie mniej dodatkowej roboty z konfiguracją frameworka do pracy. Typowa definicja modelu wyglądać może nawet tak prosto:

class User < ActiveRecord::Base
end

Powyższa definicja zakłada, że tabela w bazie nosi nazwę “users”, a jej primary key nazywa się “id”. W przeciwieństwie do innych ORM’ów, railsowy ActiveRecord nie wymaga żadnego wcześniejszego definiowania pól. Ruby bez problemu sam sobie wyciagnie wszystkie nazwy pól w sposób dynamiczny wtedy, kiedy będzie mu to potrzebne.

Nietypowe struktury

Niestety, nie zawsze jesteśmy w takiej komfortowej sytuacji, że mamy wpływ na nazewnictwo tabel i ich pól. Jednak to nie jest większym problemem, bo Rails sobie z tym radzi równie prosto.

class User < ActiveRecord::Base
  set_table_name 'uzytkownicy'
  set_primary_key 'IdUzytkownika'
end

Jak widać, dużo pracy nam nie przybyło. W tym wypadku musieliśmy tylko jawnie podać jak się nazywa nasza tabela i jak się nazywa jej klucz główny.

Korzystanie z wielu baz

W momencie tworzenia projektu aplikacji RoR generowany jest automatycznie plik konfiguracyjny zawierający połączenie do bazy danych. Przykładowy plik config/database.yml mogłby wyglądać np. tak:

default: &defaults
  adapter: mysql
  host: localhost
  encoding: utf8
  username: jakis_login
  password: jakies_haslo

development:
  database: bookstore_dev
  <<: *defaults

test:
  database: bookstore_test
  port: 3307
  <<: *defaults

production:
  database: bookstore
  socket: /var/run/mysqld/mysqld.sock
  <<: *defaults

Co robi powyższy kod? Definiuje 3 bazy: produkcyjną, roboczą i testową. (Ta ostatnia jest używana tylko do testów jednostkowych więc nie powinno się tam wstawiać żadnych istotnych danych) W w/w konfiguracji zdecydowaliśmy, że wszystkie bazy będą korzystać z MySQL >=4.1 z ustawionym wew. kodowaniem UTF-8 (zalecane). Baza produkcyjna działać będzie na Linuksie, więc zamiast portu TCP, podano uniksowy socket. Baza testowa działa na porcie 3307, a robocza na domyślnym (3306). Główna sekcja “defaults” jest wspólna dla wszystkich trzech baz zgodnie z zasadą DRY (Don’t Repeat Yourself).

Załóżmy jednak, że chcemy korzystać z dodatkowej bazy i chcemy aby z niej korzystały niektóre modele. Żaden problem. Do pliku dopisujemy tylko dodatkową definicję połączenia do innej bazy:

other:
  adapter: mysql
  host: 192.168.0.123
  database: customers
  <<: *defaults

Jak widać, możemy tą metodą tworzyć aplikację korzystającą z rozproszonych baz. (W tym wypadku baza customers leży na zupełnie innym serwerze, ale korzysta z tego samego usera i hasła.)

No dobrze, ale jak modele mają rozpoznać z jakiego połączenia korzystają? To też prosta sprawa. Domyślnym połączeniem jest to, co zdefiniowano w sekcji domyślnej. Jedynie modele, które korzystają z innego połączenia, muszą to mieć jawnie zadeklarowane w definicji, np.

class Client
  establish_connection :other
end

Model Client oraz wszystkie modele utworzone z niego na drodze dziedziczenia, będą korzystać z innego połączenia niż domyślne.

BTW, to jedna z bardziej brakujących mi cech we frameworku Django (Pylons nie ma z tym żadnego problemu). Mam nadzieję, że wkrótce to poprawią i w Django będzie można bez problemu korzystać z wielu baz.

Wspolne modele dla wielu aplikacji Rails

W tym scenariuszu mamy za zadanie stworzyć szereg aplikacji webowych korzystających z oddzielnych baz o tej samej strukturze tabel. Chcemy uniknąć powielania kodu niezbędnego dla modeli ORM. Nie mamy też czasu (ani ochoty) aby siedzieć i stworzyć definicje do wszystkich tabel wraz z wyposażeniem ich w nasze własne, dodatkowe metody uwzględniające każdą, możliwą potrzebę w przyszłości. Zamiast tego, wolimy tworzyć i rozbudowywać obiektowy opis naszej bazy dla wszystkich aplikacji w miarę ich rozwoju i wzrostu złożoności. Chcemy oby nasz model biznesowy, opisywany przez ActiveRecord, rósł stopniowo wraz ze wzrostem naszych aplikacji RoR.

Do takiego zadania, najlepiej aby wszystkie aplikacje RoR korzystały ze wspólnego repozytorium modeli. W wypadku systemów POSIX (Linux, czy MacOS-X) najprościej to rozwiązać za pomocą linków symbolicznych. Jednakże, jeśli chcemy aby nasz kod był bardziej przenośny i działał także pod Windowsami musimy inaczej podejść do tego problemu: wyniesiemy definicje naszych modeli do zewnętrznych modułów Rubiego aby domieszkowały one klasy poszczególnych modeli Rails.

Wpierw musimy dla każdej aplikacji RoR wskazać gdzie leży repozytorium z wpołdzielonymi modułami. Najlepiej wstawić do pliku conf/environment.rb zmienną globalną Rubiego o nazwie, np. $SHARED.

$SHARED = File.dirname(__FILE__)+'/../../shared/'

Konstrukcja File.dirname… jest zalecana, gdyż daje pewny i jednoznaczny dostęp do pliku bez względy na to gdzie przekopiujemy cały nasz projekt. (Identycznie należy robić pod PHP aby uniknąć problemów z ustawieniami zmiennej include_dirs. Większość pehapowego lamerstwa używa zapisu require(“plik.php”) i potem się dziwi że coś im nie działa)

Załóżmy że wcześniej mieliśmy model zdefiniowany następująco:

class Book
  set_table_name 'ksiazki'
  belongs_to :author
  def self.commented(user_id)
    self.find(:all, :conditions=>['user_id=?',user_id])
  end
end

Mamy tu więc nie tylko nazwę tabeli niezgodną z konwencją Railsów, ale także dodatkową metodę klasową. Wpierw zróbmy z tego moduł:

module BookModule
  def self.extended(c)
    c.set_table_name 'ksiazki'
    c.belongs_to :author
  end
  def commented(user_id)
    self.find(:all, :conditions=>['user_id=?',user_id])
  end
end

Kod korzystający z tego modułu przyjmie postać:

if RAILS_ENV == "development"
  load $SHARED + 'models/book.rb'
else
  require $SHARED + 'models/book.rb'
end

class Book< ActiveRecord::Base
  include BookModule
  extend BookModule
end

Górny fragment jest potrzebny jeśli chcemy aby Webrick/Mongrel w trybie development się przeładowywał podczas zmiany kodu w Ruby. “include” włącza metody instancji. Zaś “extend” włącza wszystkie emetody modułu _BookModule _do klasy jako jej metody klasowe. Zwróć uwagę, że w module nie definiowaliśmy ich jako z prefiksem self. Co prawda, efektem ubocznym tego przykładu jest to, że metoda commented() będzie możliwa do uruchomienia jako metoda zarówno instancji jak i klasy, ale to raczej nie ma żadnego znaczenia.

Posted in  | Tagi , ,  | 11 comments

Comments

  1. Avatar lopex powiedział about 6 hours later:

    bardzo polecam plugin easy-where dzięki któremu:

    self.find(:all, :conditions=>[‘user_id=?’,user_id])

    przyjmie postać:

    self.find_where(:all) |book| user_id book.user_id end

    ładne nie ? Ale można robić dużo większe cudawianki (bardzo czytelne):

    Foo.find(:all) do id = [1, 3, 8] foo == ‘other bar’ fiz =~ ‘faz‘ bar > 4 end

    warunki można również tworzyć tak:

    :condtions => c{|foo| foo.bar > 4}.to_sql

    można je też dodawać przez + i tworzyć alternatywy przez |.

    http://brainspl.at/articles/2006/01/30/i-have-been-busy http://brainspl.at/articles/2006/06/30/new-release-of-ez_where-plugin

    a ostatni feature to już jest bajer! http://brainspl.at/articles/2006/10/03/nested-joins-and-ez-where-update

  2. Avatar lopex powiedział about 6 hours later:

    znowu mi nowe linie zjadło :(

  3. Avatar lopex powiedział about 6 hours later:

    i poprawka:

    self.find_where(:all) do |book| user_id == book.user_id end

  4. Avatar Jarosław Zabiełło powiedział 1 day later:

    To wygląda ciekawie, jednak mam wrażenie, że (1) to bardziej bajer niż jakieś znaczące ulepszenie jakości ORM’a (vide znacznie lepszy ORM jaki ma Django), (2) ta cała idea pluginów zmieniających Railsy podoba mi się tak sobie, bo kod staje się trochę nieprzewidywalny. Tutaj bym bronił podejścia pythonowego. Mniej magii i zgadywania co do czego służy przy zachowaniu równej prostoty kodu. A jeśli taki “feature” jest aż tak dobry, to niech zostanie dołączony do głównego kodu Railsów.

  5. Avatar lopex powiedział 1 day later:

    z tym ORM django bym nie przesadzał ;) zobacz sobie na paskudną obsługę LOOKUP_SEPARATOR i QUERY_TERMS – to jest dużo gorsze od method_missing ;)

  6. Avatar maniel powiedział 2 days later:

    W przeciwieństwie do innych ORM’ów, railsowy ActiveRecord nie wymaga żadnego wcześniejszego definiowania pól. Ruby bez problemu sam sobie wyciagnie wszystkie nazwy pól w sposób dynamiczny wtedy, kiedy będzie mu to potrzebne.

    Dla mnie w ORM ważna jest przenośność i automatyka tworzenia tabel, w ActiveRecord, o ile dobrze pamiętam, trzeba tworzyć schemat, oddzielajac go od modelu. Mi to z deczka nie pasuje dlatego bardziej podoba mi się nitrowe OG gdzie w modelu definiuję pola, po czym automatycznie utworzona mi zostanie tabela odwzorowująca [odwzorowywująca?] tenże model. Tak samo jest z resztą w django. Ale kto co lubi, ja jestem za takim właśnie rozwiazaniem, zamiast ręcznego tworzenia tabeli albo też pisania iddzielnie jakiegoś skryptu który tworzy mi tabele:)

  7. Avatar Jarosław Zabiełło powiedział 2 days later:

    W Railsach też możesz tworzyć strukturę bazy w czystym Ruby i Rails wygeneruje odpowiedni kod SQL tworząc tabelki. Mało tego, taką operację możesz wersjonować i niedestrukcyjnie (dla danych) cofać się do wcześniejszych wersji bazy. To się nazywa Migrations.

  8. Avatar Jarosław Zabiełło powiedział 2 days later:

    Nie wiem co ci się nie podoba w LOOKUP_SEPARATOR i QUERY_TERMS? Może to rzecz gustu, ale mnie się to znacznie bardziej podoba od klepania SQL’a dla :conditions w Active Record.

    Nie rozumiem o co ci chodzi z tym method_missing?

    Nie wiem jak to jest rozwiązane w w ActiveRecord, ale Django stosuje lazy queries, odpytuje się bazy dopiero w momencie pobierania danych. To jest znacznie lepsze rozwiązanie, bo pozwala na bardziej wydajną implementację zapytań (nic dziwnego, że Django jest znacznie szybsze od Railsów).

    ActiveRecord od razu zapytuje się bazy i zwraca listę :(
    p MyModel.find(:all).class 
    >>> Array
    Django zaś zwraca obiekt QuerySet:
    print type(MyModel.objects.all())
    >>> <class 'django.db.models.query.QuerySet'>
  9. Avatar lopex powiedział 2 days later:

    No i właśnie po to są pluginy. Nie znalazłem w żadnym innym framewarku nic lepszego niż ease-where (no.. w lispie da sie lepiej).

    jest też plugin: http://agilewebdevelopment.com/plugins/paginating_find ale nie jest on całkiem lazy.

    Można się pobawić enumeratorem z Rubiego – taki odpowiednik Pythonowych generatorów, dla uzyskania pełnego lazy loading (ale niestety w bebechach AR)

  10. Avatar maniel powiedział 3 days later:

    ooo, właśnie, s/schematy/migrations/ zapomniałem jak to się zwie:P A co do odpytywania bazy dopiero przy pobieranbiu danych, to jest bardzo dobry pomysł. Nie wiem czemu w AR tego nie ma, pewnie dlatego ze nie zostal pomyślany do rozwiązań typu enterprise podczas gdy django bylo od początku pisane z tym na myśli [afaik]. Przydatna to rzecz gdy ktoś wsadza dużo danych do bazy.

  11. Avatar lopex powiedział 3 days later:

    enterprise to może Zope a i tak nuxeo z niego zrezygnował, nie sądze aby django/rails były choć troche enterprisey. W tej chwili eksperymentuję z jruby/ejb – to już bardziej…

(leave url/email »)

   Pomoc języka formatowania Obejrzyj komentarz