Elastyczność Railsów
Posted by 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
endPowyż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'
endJak 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
<<: *defaultsCo 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
<<: *defaultsJak 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
endModel 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
endMamy 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
endKod 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
endGó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.


Kanały IRC![[Dilber w Onecie]](/images/larry.png)


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
znowu mi nowe linie zjadło :(
i poprawka:
self.find_where(:all) do |book| user_id == book.user_id end
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.
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 ;)
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:)
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.
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ę :( Django zaś zwraca obiekt QuerySet: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)
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.
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…