Co nowego w Rails3?

Posted by Piotr Sarnacki Sun, 03 May 2009 18:10:00 GMT

Ponad 4 miesiące temu światem Ruby’iego zatrzęsła wiadomość o połączeniu się 2 popularnych frameworków – Ruby on Rails i Merba. Dzisiaj, tylko kilka dni dzieli nas od ujrzenia pierwszych aplikacji, które będą napisane w Rails 3. Carl Lerche (twórca między innymi routera do merba) oraz Yehuda Katz na tegorocznym RailsConf będą mówić o Rails3 i mountable apps.

Na stabilną wersję będzie trzeba jeszcze trochę poczekać, ale trzymam kciuki, żeby na RailsConf rzeczywiście udało im się pokazać działającą aplikację.

W tym poście chciałbym krótko opisać to co ma się znaleźć w Rails 3. Z niemałą niecierpliwością śledzę newsy i doniesienia o nowych railsach i nie mogę się już doczekać niektórych usprawnień.

ActionORM (wcześniej ActiveORM)

Zwolennicy Merba bardzo podkreślają w swoim frameworku niezależność od konkretnych technologii. Było to reklamowane głównie jako “ORM agnosticism” czyli możliwość podpięcia do Merba jednego z wielu “ORMów” (DataMapper, ActiveRecord, Sequel) w porównaniu do Railsowej monolityczności w postaci jednego słusznego ActiveRecorda.

Jaki właściwie jest problem w używaniu innego ORMa w naszej aplikacji? Na poziomie modeli i kontrolerów nie ma to z reguły większego znaczenia. Przecież Railsów nie obchodzi czy klasa w katalogu app/models dziedziczy po ActiveRecord::Base, czy dołącza moduł: include DataMapper::Resource

Kłopot zaczyna się kiedy chce się korzystać z wielu helperów, które jako argument przyjmują obiekt ActiveRecord. Najprostszy przykład:

<% form_for @article do |f| %>
  <%= f.text_field :title %>
<% end %>

Jeżeli @article nie jest obiektem AR, to poprawność działania tej metody zależy tylko od tego czy interfejs danego ORMa tutaj używany jest taki sam. W tym akurat przypadku problemem może być sprawdzanie błędów w walidacji. Railsy domyślnie dodają wrapper do pól z błędami, więc jeżeli ORM ma inny interfejs do pobrania informacji o błędach, to metoda po prostu nie zadziała.

W tym momencie jest to w Railsach zrobione z użyciem alias_method_chain i jeżeli obiekt nie ma zaimplementowanej metody errors, to tag pozostaje niezmieniony, więc można samemu dołączyć swoją własną implementację lub dodać potrzebne metody do modelu. Jednak wszyscy chyba się zgodzą, że nie jest to najbardziej elegancki sposób.

Jeżeli chodzi o Merba, to sprawa wygląda tylko trochę lepiej niż w railsach. Jest co prawda wsparcie dla 3 najpopularniejszych ORMów, ale implementacja nie pozwala na łatwe dodanie kolejnych ORMów bez grzebania w kodzie. Jeżeli ORM, którego chcesz używać ma interfejs zgodny z DataMapperem, Sequelem lub ActiveRecordem, to wszystko jest ok, ale jeżeli jest to coś innego, to masz problem.

Odpowiedzią na to ma być ActionORM. Czym jest ActionORM? Jest to proste proxy dla obiektów. ActionORM udostępnia API takie jak ActiveRecord (dzięki temu w kodzie railsów nie będzie trzeba wiele zmieniać). W prostych słowach ActionORM ma sprawić, że obiekty innych ORMów będą wyglądały jak obiekty ActiveRecord (czyli klasyczne wykorzystanie duck typing)

Tutaj jest przykład proxy dla Datamappera: http://github.com/lancecarlson/rails/blob/0faae5b971c3dbf3b1c4ead19504580233bbc7fa/activeorm/lib/active_orm/proxies/datamapper_proxy.rb

Różnice są jak widać niewielkie, ale podejrzewam, że będzie można dodać więcej metod, które się różnią pomiędzy implementacjami (oczywiście w granicach rozsądku, sprowadzanie całego interfejsu do jednego przypadku było by pozbawione sensu, chodzi o proste operacje, które każdy ORM ma rozwiązane podobnie, ale różni się API).

Rack Middleware

Railsy od jakiegoś czasu wykorzystują w pełni dobrodziejstwa Rack. Co więcej, różne części frameworka są zaimplementowane jako rack middlewares. Kontroler, router, obsługa sesji i wiele innych rzeczy to teraz po prostu kolejne middlewares (przepraszam za te anglojęzyczne wstawki, ale naprawdę nie mam pojęcia jak to ładnie przetłumaczyć). Dzięki temu i dodaniu Rails metal aplikacje są coraz bardziej modularne.

Programiści railsów dążą w tej chwili do jeszcze szerszego wykorzystania racka. Wszystko to idzie w stronę jak najłatwiejszego połączenia komponentów różnego typu. Na przykład możliwość podłączenia do routera kontrolerów railsowych, aplikacji sintatry i aplikacji merba (jako jedną aplikację oczywiście).

Optymalizacja i refaktoryzacja

Największą i najlepszą według mnie zmianą w railsach jest refaktoryzacja kodu ActionPack. Na czym ona polega? W bardzo prostych słowach: stworzony jest AbstractController, z którego dziedziczyć będą inne kontrolery, a w samym kodzie jest zastosowana dużo większa enkapsulacja. Jakie to ma znaczenie dla użytkowników railsów? Będzie istniał jeden interfejs dla wszystkich kontrolerów, dzięki czemu na przykład ActionController i ActionMailer będą mogły współdzielić funkcjonalność (żeby nie iść daleko, dopiero niedawno do mailera dodana została obsługa layoutów). Bardzo łatwe będzie też dodanie Parts znanych z merba (PartsController również dziedziczy w Merbie z AbstractControllera).

W chwili obecnej podobną funkcjonalność do Merb Parts udostępniają Rails Cells, ale nie jest to rozwiązanie idealne (implementacja jest zagmatwana, w cellach widoczne są zmienne instancji z kontrolerów i innych celli, podejrzewam, że jest to wolniejsze niż parts).

Co jeszcze? Yehuda Katz wziął się między innymi za optymalizację callbacków, czy bloku respond_to. Polecam również inne artykuły na temat refaktoryzacji na jego blogu.

Na pewno szybszy będzie też router. W chwili obecnej największym problemem jest to, że router generuje bardzo dużo metod (wszystkie metody _url i _path), co w rezultacie przyczynia się do bardzo wolnego startu aplikacji. W czasach mongreli nie miało to dużego znaczenia, bo aplikacje były odpalane raz i tylko sporadycznie restartowane. Teraz kiedy na wielu serwerach zainstalowany jest mod_passenger, czas startu aplikacji jest dużo ważniejszy – instancje, które nie dostają requestów przez 5 minut (domyślne ustawienie) są wyłączane dzięki czemu nie zajmują zasobów.

Nowy router można przetestować już teraz. Po więcej odsyłam na the merbist

Mountable apps

Jest to jedna z tych rzeczy, na które czekam w Railsach 3 najbardziej. Dosłownie liczę godziny do rails conf i prezentacji na ich temat (mam nadzieję, że będą nagrywane, bo niestety nie miałem wolnych kilku tysięcy złotych, żeby się pojawić osobiście na konferencji ;-).

Jakieś namiastki mountable apps są już dostępne w postaci Rails Engines i Merb Slices. Jest to jakieś rozwiązanie, ale na pewno dalekie od idealnego. Żeby skorzystać z którejś z tych opcji trzeba napisać specyficzny rodzaj aplikacji, a każde z tych rozwiązań jest w niektórych miejscach niedopracowane. Celem mountable apps będzie możliwość łatwego połączenia 2 aplikacji bez żadnych zmian w kodzie.

API dla pluginów

Jedną z rzeczy, którą wypromował Merb jest publiczne i prywatne api. Dzięki temu pisanie pluginów nie wiąże się tam z używaniem alias_method_chain i ogólnie pojętym monkey patchingiem na każdym kroku. A jeżeli czegoś nie da się zrobić używając tylko publicznego api, to jest to bug. Jest to chyba jedna z trudniejszych rzeczy w całej refaktoryzacji. Nie wiadomo dokładnie czego będą potrzebować twórcy pluginów, niektóre metody trzeba będzie dopiero stworzyć.

Ogromnym plusem takiego podejścia jest to, że twórcy frameworka nie muszą się martwić o prywatne metody, ważne żeby publiczne API się nie zmieniało. Dzięki temu pluginy napisane zgodnie z wytycznymi, czyli nie używając prywatnego API nie będą się wysypywały na każdej kolejnej wersji (co bardzo często można zaobserwować w tym momencie), a jeżeli coś się zmieni w publicznym API przy okazji większych zmian, to twórcy pluginów będą mogli się o tym łatwo dowiedzieć bez śledzenia commitów na githubie.

Podsumowanie

Jestem zdania, że połączenie merba i railsów to jedna z najlepszych rzeczy, która mogła się przytrafić programistom webowym. Railsy będą szybsze, bardziej modularne i łatwiejsze w rozszerzaniu. Tylko czekać na wyniki refaktoryzacji :)

Postaram się w najbliższych tygodniach pisać o tym co dzieje się z Railsami3, jak idą prace i co jeszcze ciekawego będzie można zobaczyć.

Posted in  | Tags , , , , ,  | 7 comments | no trackbacks

Refaktoryzacja Railsów

Posted by Piotr Sarnacki Sat, 03 Jan 2009 20:45:00 GMT

Niestety nie będzie to wpis o tym jak refaktoryzować swoją aplikację, ale raczej post o refaktoryzacji samego frameworka w ramach przygotowań do Rails 3.

Railsy pożarły mózg Merba i posiądą jego tajemne moce (zachęcam do wysłuchania tego podcastu, chłopaki od railsenvy genialnie mówią o tym co się dzieje w świecie railsów) – o tym już każdy na pewno wie. Czy to dobrze? Oczywiście, że tak :)

Jeżeli ktoś dalej ma problemy z tą decyzją (podejrzewam, że może tak być w wypadku fanów merba), polecam prezentacją Living with legacy software wygłoszoną przez Davida Heinemeiera Hanssona – nie jest w żaden sposób bezpośrednio związana z połączeniem, ale zmienia podejście do aplikacji i ich kodu.

Ale nie o to, nie o to, nie o to

Yehuda Katz ostro wziął się do pracy jeszcze podczas świąt i co jakiś czas wrzuca na bloga informacje o tym jak refaktoryzuje i jakie ma plany wobec Railsów. Informacje traktujące o projektowaniu, refaktoryzacji i architekturze ciężko jest znaleźć w sieci, więc gorąco polecam śledzenie tego co pisze Yehuda – do tej pory można przeczytać między innymi o enkapsulacji ActionView i ActionController czy o optymalizacji respond_to. Dodajcie sobie tego bloga do czytnika, naprawdę warto.

Co najlepsze, wiele z tych zmian dotyka tylko wewnętrznej implementacji railsów, więc można liczyć, że będą one dostępne dla ruby on rails 2.3.0.

Posted in ,  | Tags , , , , , ,  | no comments | no trackbacks

Named scope

Posted by Piotr Sarnacki Fri, 03 Oct 2008 10:37:00 GMT

W railsach od jakiegoś czasu można używać named_scope. Jest to bardzo fajny mechanizm umożliwiający łatwiejsze skonstruowanie zapytania używając wcześniej zdefiniowanych metod. Wcześniej był dostępny jako plugin has_finder.

Wygląda to mniej więcej tak:

  named_scope :active, :conditions => { :active => true }
  named_scope :paid, :conditions => { :paid => true }
  named_scope :recent, lambda { { :conditions => ['created_at >= ?', 1.week.ago] } }

Po zdefiniowaniu takich sope’ów (macie pomysł jak to spolszczyć?) można ich używać w ten sposób (zakładam, że scope’y zostały zadeklarowane w modelu Product):

  Product.active.paid.recent

Jak można łatwo zauważyć, scope’y można łączyć w łańcuchy. Dzięki takiej konstrukcji otrzymujemy prosty i treściwy kod, który łatwo zrozumieć. Teraz coś lekko trudniejszego:


  named_scope :in_category, lambda { |category| { :conditions => { :category_id => category } } }

  # i użycie
  Product.active.recent.in_category(category)

O co chodzi? Jako 2 argument można przekazać lambdę, która zostanie wywołana przy użyciu scope’a i powinna zwrócić Hash.

Ale to nie są jedyne zastosowania scope’ów. Można do nich przekazać też inne parametry, które może przyjąć metoda find.

Na przykład order, offset i limit:

  named_scope :order, lambda { |*args| { :order => args.first || "created_at DESC" } } 
  named_scope :offset lambda { |offset| { :offset => offset } } 
  named_scope :limit, lambda { |*args| { :limit => args.first || 10 } } 

  # i użycie:
  Product.active.order("id DESC").offset(10).limit

W pierwszym i trzecim scope’ie zastosowałem trick, który pozwala na użycie zdefiniowanej wartości domyślnej w razie gdy żadna nie zostanie podana. `*args` pakuje argumenty do tablicy. Jeżeli args.first zwróci nil, to znaczy, że tablica jest pusta, czyli nie zostały podane żadne argumenty i trzeba zaaplikować wartość domyślną.

Coś jeszcze? Fajnie by było, żeby można było sortować nie tylko po wartościach z jednej tablicy… a do tego wypadałoby użyć joins/include. Nic prostszego.


  named_scope :joins, lambda { |joins| { :joins => joins } } 

  # a teraz można tak:
  Product.active.joins(:user).order('users.email')

How cool is that?

No i na koniec coś co ostatecznie przekonało mnie, że named_scope jest genialnym wynalazkiem. Jeżeli ktoś robił kiedyś formularze, w których można wybrać kilka opcji i na ich podstawie trzeba zbudować zapytanie, zapewne wie, że jest to nieco upierdliwe. Używając `named_scopes` można to zrobić bardzo łatwo.

Ryan Bates napisał plugin scope_builder, dzięki któremu można to zrobić jeszcze łatwiej. Przypuśćmy, że mamy formę, w której można zaznaczyć kilka pól i po ich wysłaniu należy na ich podstawie pobrać odpowiednie rekordy z bazy.


  # przygotowujemy listę parametrów, które użytkownik może ustawić
  parameters = [:active, :paid, :recent, :title]
  # Tworzymy scope
  @products = Product.scope_builder
  # teraz można sprawdzić, które pola zostały zazanczone
  parameters.each do |param|
    @products.send(param) if params[param]
  end
  # na koniec można na przykład dodać paginację
  @products = @products.paginate(:per_page => 10, :page => params[:id])

Dla mnie genialne. :) Można dzięki temu łatwiej tworzyć zaawansowane wyszukiwanie, sortowanie i inne tego typu rzeczy.

Oczywiście najlepiej jest zamknąć taki kawałek kodu jako metodę modelu, zgodnie ze skinny controller, fat model, ale to pozostawię jako ćwiczenie ;-)

Posted in ,  | Tags , ,  | no comments | no trackbacks

Po dłuższej przerwie

Posted by Piotr Sarnacki Tue, 30 Sep 2008 14:38:00 GMT

Przez jakiś czas nie pisałem nic na blogu. Długo zbierałem się do napisania czegokolwiek, od dawien dawna miałem przetłumaczyć wpisy na moim angielskojęzycznym blogusiu. Tak bardzo mi się nie chciało, że jak tylko siadałem do kompa z zamiarem przetłumaczenia czy napisania czegoś podobnego coś mnie odrzucało.

Dlatego, żeby więcej się nie męczyć podsumuję ten temat i podam linki – większość ludzi, którzy tutaj trafią i tak na pewno zna angielski.

Napisałem apache upload progress module do apacha. Moduł, dzięki któremu można pobrać dane o wysyłanych plikach. Format odpowiedzi oparłem o moduły tego typu dla nginxa i lighttpd dlatego jeżeli wcześniej ktoś używał któregoś z nich, przesiadka będzie całkowicie bezbolesna. Sam używałem wcześniej nginx upload progress (głównie dlatego napisałem moduł do apacha) i po przerzuceniu aplikacji na apacha z nowym modułem nie trzea było zmieniać ani jednej linijki. Na angielskim blogu zamieściłem opis instalacji i konfiguracji modułu.

Żeby można było w miarę łatwo używać modułu napisałem także pluginy do “prototype’a”http://github.com/drogus/prototype-upload-progress/tree/master i jquery, które obsługują pasek postępu. Przykłady są w repozytoriach, umieściłem je także na serwerze dla obczajenia w akcji. Popełniłem także tekst o tym jak hackowałem plugin, żeby działał w safari – safari w tym momencie nie było wcale lepsze od IE… nawet powiedziałbym, że gorsze. Swoją drogą plugin nie działa w najnowszej operze (działał w 9.5 zdaje się), więc jeżeli ktoś ma chwile czasu i mógłby sprawdzić dlaczego i co można zrobić, żeby działał, to może wnieść swój wkład w rozwój (dodam, że nie jest to ta sama kwestia co dla safari, bo to już sprawdziłem) – nie sądzę żebym miał teraz czas sam na tym siedzieć.

Dodatkowo napisałem jeszcze tekst o tym jak można fajnie uatrakcyjnić aplikację używając jQuery. Część pierwsza. Część druga będzie jak będę miał więcej czasu, czyli pewnie niezbyt szybko.

Pozdrowienia ze słonecznego Wrocławia, gdzie mam zamiar mieszkać przez najbliższy rok (miła odmiana od Pruszkowa :).

Posted in , ,  | Tags , , , ,  | no comments | no trackbacks

Mod passenger i Ruby Enterprise Edition

Posted by Piotr Sarnacki Tue, 17 Jun 2008 18:50:00 GMT

Dzisiaj opowiem wam bajeczkę o tym jak mod_passenger zeswatał ze sobą Apache’a i Railsy. :)

Jeżeli ktoś sądzi, że z tego związku nic nie będzie, to rzucam kilka zalet:
  • nie trzeba bawić się w konfigurację proxy i martwić o zajęte porty
  • nie trzeba monitorować mongreli (ja używałem do tego goda)
  • ogólna prostota użycia
  • dzięki ruby enterprise można zaoszczędzić 33% pamięci
  • upload buffering, czyli nie musimy się martwić o upload dużych plików – pliki są przesyłane do aplikacji railsowej dopiero, gdy zostaną w całości uploadowane na serwer
  • fair load balancing – zapytania są wysyłane do procesów, które mają najmniej klientów w kolejce. żeby uzyskać coś takiego używając nginxa trzeba było instalować dodatkowy plugin
  • liczba instancji aplikacji nie jest sztywno określone. używając nginxa trzeba było przewidzieć jaka liczba serwerów railsów będzie potrzebna dla danej aplikacji, jeżeli na dany serwis wchodziło mało osób niewykorzystane serwery zjadały pamięć. mod_passenger uruchamia kolejne instancje w miarę potrzeb.

Dodatkowo napisałem apache upload progress module , dzięki czemu z mod_passengerem można sklecić pasek postępu wysyłania plików na serwer (forma dokładnie taka sama jak z Nginx Upload Progress, więc jeżeli ktoś go używał, to nawet nic nie będzie musiał zmieniać w skryptach).

Mod Passenger

Aktualnie najnowszą wersją mod_passengera jest 1.9.1 (RC2), instrukcja instalacji jest na blogu phusion.

Po odpaleniu instalatora użytkownik jest prowadzony za rączkę, więc nikt nie powinien mieć probleów – lubię takie podejście, nie muszę się zastanawiać nad tym co i jak mam zrobić i kopiować i wklejać kolejnych komend.

Mod passenger sprawuje się na serwerze bardzo fajnie, do tej pory nie miałem żadnych problemów. Żeby zrestartować aplikację rails wystarczy utworzyć plik tmp/restart.txt w katalogu aplikacji.

Po instalacji można jeszcze dodać w configu apacha linijkę:
PassengerMaxPoolSize X

Zamiast X wstawiamy maksymalną liczbę instancji aplikacji odpalonych na raz. W dokumentacji twierdzą, że dla VPSa z 256 megabajtami pamięci ram dobrą wartością będzie tutaj 2, a dla serwera dedykowanego z 2GB ramu 30. Większa liczba aplikacji to więcej zajętego ramu, ale też więcej requestów do obsłużenia w danej chwili.

Ruby Enterprise Edition

O ile o mod passengerze jest dużo informacji, to o ruby enterprise edition jeszcze niewiele. Według niektórych nazwa jest nietrafiona i niefajna, niektórym nie podoba się, że phusion chce się na REE wypromować.

Mi to w zasadzie wszystko jedno jak się chłopaki z Phusion promują, kto na tym zarobi i jaka jest nazwa, o ile ta wersja będzie dobrze działała i rzeczywiście zmniejszała zużycie pamięci.

Żeby zainstalować ruby enterprise edition należy ściągnąć paczkę (najnowszą wersję paczki można znaleźć tutaj):
wget http://rubyforge.org/frs/download.php/38777/ruby-enterprise-1.8.6-20080623.tar.gz
Następnie ją rozpakować:
tar -zxvf ruby-enterprise-1.8.6-20080623.tar.gz
i uruchomić installer:
./ruby-enterprise-1.8.6-20080623/installer
Tyle mówi opis na stronie REE, ale to jeszcze nie wszystko. Na początek można stworzyć dowiązanie symboliczne dla REE:
ln -sf /opt/ruby-enterprise-1.8.6-20080623/bin/ruby /usr/bin/ruby-enterprise
Użytkownicy gentoo muszą również skopiować plik auto_gem, który automatycznie jest w tej dystrybucji dodawany do RUBYOPT:
cp /usr/lib/ruby/site_ruby/auto_gem.rb /opt/ruby-enterprise-1.8.6-20080623/lib/ruby/site_ruby

Po zainstalowaniu rubiego instaluje się też kilka gemów, ale to tylko podstawowe – resztę trzeba zainstalować samemu. Oczywiście można odpalić gem list i instalować kolejne gemy w danych wersjach, ale od czego mamy rubiego. Dosłownie w 2 minuty napisałem prosty skrypcik, który wyciąga nazwy i wersje gemów po czym instaluje je dla Ruby Enterprise Edition:


`gem list`.split("\n").each do |line|
  if line =~ /([0-9a-zA-Z_\-]*) \((.*)\)/
    gem, versions = $1, $2.split(", ")
    versions.each do |version|
      puts "Installing #{gem}, version: #{version}" 
      puts `ruby-enterprise /opt/ruby-enterprise-1.8.6-20080623/bin/gem install #{gem} -y --version '#{version}' 2>&1`
    end
  end
end
Teraz wystarczy dodać w configu apacha linijkę:
RailsRuby /opt/ruby-enterprise-1.8.6-20080623/bin/ruby

I możemy cieszyć się naszym własnym wypicowanym apachem z mod_passengerem w korporacyjnej wersji ;-)

Posted in  | Tags , , , ,  | no comments | no trackbacks

mod_rubinius

Posted by Piotr Sarnacki Thu, 31 Jan 2008 09:09:00 GMT

Jakiś czas temu pisałem ,że mogą szykować się zmiany w deploymencie railsów. Dzisiaj przeczytałem, że Ezra Zygmuntowicz zatrudnił szóstego programistę, którego zadaniem będzie praca nad mod_rubinius dla serwerów Nginx i Apache. Więcej w podcaście z udziałem Ezry

Mały cytat:
More interesting things from the podcast:
  • In like one night Evan Phoenix implemented a multiple Rubinius VM running in single OS process in native threads and passing each other messages like it happens in Erlang). This can lead to a great solution to shared hostings and Ruby deployment problem David Hansson wrote about recently.
  • Rubinius may support native code compilation along side with bytecode that Rubinius VM can run. Sounds interesting. Ryan Davis is working on Rubinius at Engine Yard and his Ruby2C experiments may be useful.

Posted in  | Tags , , , , , , ,  | no comments | no trackbacks

Rails 2.0 i js.erb

Posted by Piotr Sarnacki Tue, 15 Jan 2008 20:36:00 GMT

Niedawno pisałem o nieinwazyjnym kodzie javascript w railsach. Wspomniałem tam o pluginie MinusMor, który dodaje pliki ejs. Czyli kod javascript parsowany erbem. Zainstalowałem plugin w mojej aplikacji przeniesionej na rails 2.0. Kod pluginu nie działa… zacząłem przeglądać kod railsów, żeby poprawić minusmor (w internecie nie mogłem znaleźć poprawki). Po chwili przemyślenia sprawdziłem co dzieje się gdy wyrzucę plik rjs. Komunikat o błędzie:


Missing template pages/index.js.erb in view path

Git! W railsach 2.0 możemy przecież wskazać jakiego parsera mają użyć.

Tworzymy plik index.js.erb. I jeszcze mała zmiana, ustawiamy brak layoutu, żeby renderował się sam template:

respond_to do |format|
  render.html
  render.js { render :layout => false }
end

I to wszystko! Po prostu działa.

Nawet lepiej. Można zdefiniować layout, na przykład:


render.js { render :layout => 'jquery' }
a w nim można na przykład napisać:

(function ($) {
  <%= yield %>
})(jQuery);

I można używać w template’ach js.erb $ pomimo tego, że wcześniej użyło się jQuery.noConflict.

Przeszukałem później jeszcze net w poszukiwaniu informacji na ten temat i znalazłem artykuł na mad.ly o dodaniu helperów z MinusMor

Posted in  | Tags , ,  | no comments | no trackbacks

Przyjazne adresy w Ruby on Rails

Posted by Piotr Sarnacki Sat, 12 Jan 2008 10:47:00 GMT

W dzisiejszych czasach, kiedy SEO dla niektórych jest ważniejsze od treści przyjazne adresy można zobaczyć na większości stron. Dlaczego nie wrzucić ich do naszej aplikacji? Nawet jeżeli komuś nie zależy na SEO, przyjazne adresy są… przyjazne! Użytkownik dostający linka http://foo.com/article/kolejny-artykul-o-naszej-klasie będzie miał szansę zastanowić się sekundę i nie kliknąć (nie to żebym miał coś do naszej klasy, ale boję się ostatnio lodówkę otworzyć, jeszcze szynka zacznie nawijać kogo dodała do znajomych). Same korzyści!

Dodanie przyjaznych adresów jest w aplikacji railsowej bardzo proste jeżeli trzymamy się kilku zasad. Jak to działa? ActiveRecord::Base, po którym dziedziczą udostępnia metodę to_param, która jest wykorzystywana przy generowaniu adresów.

W praktyce wygląda to tak, że jeżeli napiszemy:
link_to @article.name, articles_path(@article)
#albo
link_to @article.name, :controller => "articles", :action => "show", :id => @article

wywołana zostanie metoda to_param i @article zostanie zamieniony na jego id. Można to wykorzysta do naszych niecnych celów.

Można nadpisać to_param
  def to_param
    "#{id}-#{name.gsub(/[^a-z0-9]+/i, '-')}" 
  end

Od teraz zamiast id będzie generował się string zawierający name, na przykład: “11-tytuł-artykułu”. Hamerykanie mają mniejszy problem, bo nie mają znaków diakrytycznych i mogą to tak zostawić. gsub zamieni wszystkie znaki nie wchodzące w skład alfabetu na myślnik. U nas jest gorzej, bo nie chcemy mieć adresu: “11-tytu-artyku-u”. I weź teraz zgaduj gdzie dorzucić ogonki. Z pomocą przychodzi Obie Fernandez, który napisał najfajniejszą jaką do tej pory widziałem metodą zamieniającą znaki diakrytyczne na odpowiadające im litery alfabetu Wrzuciłem na serwer wersję z polskimi znakami diakrytycznymi. Taki plik wystarczy wrzucić do folderu initializers dla Railsów 2.0.x, albo do katalogu lib dla 1.2.x (w tym wypadku trzeba też w environment.rb dodać linijkę require ‘ascii’). Zrobiłem jeszcze jedną modyfikację – to_url_format powinien moim zdaniem w miejsce spacji i innych znaków wrzucać myślniki. Lepiej wygląda “tytul-artykulu” niż “tytulartykulu”.

Dzięki temu String udostępnia 2 nowe metody: `to_url_format` i `to_ascii`:
"zażółć gęślą jaźń".to_ascii #=> "zazolc gesla jazn" 

"zażółć gęślą jaźń".to_url_format #=> "zazolc-gesla-jazn" 
Teraz wystarczy zmodyfikować lekko to_param:
  def to_param
    "#{id}-#{name.to_url_format}" 
  end

Dzięki temu urle będą miały upragnioną formę: “12-tytul-artykulu”.

Tylko po co to id na początku? Dzięki temu obędzie się bez żadnych zmian w kontrolerach. Taki string zostanie przed wrzuceniem do bazy automatycznie skonwertowany na liczbę całkowitą. Czyli kolejna rzecz, która “po prostu działa” automagicznie. Jeżeli id będzie potrzebne w jakimś innym miejscu, w którym nie nastąpi konwersja można to zrobić ręcznie: params[:id].to_i, bo:
"11-jakis-napis".to_i #=> 11

Jaki jest minus takiej metody? Adresy mają w sobie id i tylko na podstawie tego id jest pobierany artykuł, więc czy wpiszemy /articles/11-tytul-artykulu, czy /articles/11-tralalala pobierze się ten sam artykuł.

Żeby temu zapobiec najlepiej zrobić dodatkową kolumnę, na przykład “permalink” i wpisywać do niej przekształcony adres. Np. article.permalink = article.name.to_url_format . I zamiast po id szukać po kolumnie permalink: Article.find_by_permalink(params[:id]). Oczywiście z metody to_param też trzeba usunąć id :)

Nie każdemu to drugie podejście będzie potrzebne. Dla mnie wersja z id jest o tyle lepsza, że jak nawet ktoś nie skopiuje całego adresu, albo pomyli się przy wpisywaniu (tak, czasami zdarza się, że ktoś dyktuje jakiś adres), to jest duża szansa, że dotrze na dobrą stronę. Poza tym istnieje wtedy możliwość wklejenia adresu bez dalszej części, która może być dość długa – może nie trzeba będzie korzystać z serwisów typu tinyurl.

Wrzuciłem nową wersję skryptu działającą z nowszymi wersjami Ruby on Rails

Posted in  | Tags , , , , , ,  | 2 comments | no trackbacks

Zmiany w deploymencie Ruby on Rails na horyzoncie?

Posted by Piotr Sarnacki Thu, 10 Jan 2008 18:16:00 GMT

Na blogu dreamhostu narzekają na bóle hostowania railsów. DHH (himself™) odpowiada, że developerzy z Railsów nie mają potrzeby zmian idących w kierunku ułatwienie deployment na dzielonych hostingach, ale chętnie pomogą jeżeli dostaną jakieś propozycje współpracy nad tym problemem. Na RubyInside jest artykuł, w którym autor zastanawia się co można poprawić.

Z jednej strony rozumiem podejście DHH: “nie pracujemy na dzielonych hostingach, więc nigdy nie mieliśmy potrzeby poprawy czegokolwiek”, a z drugiej strony od razu przychodzi na myśl mongrel napisany przez Zeda Shaw. W czasach “pre-mongrel” trzeba było w bólu i cierpieniach próbować zmusić lighttpd (albo apacha, wersja dla prawdziwych masochistów) do hostowania railsów via FastCGI. Gdyby nie Zed prawdopodobnie długo jeszcze przyszłoby męczyć się z takimi configami, nie sądzę, żeby DHH myślał nad jakimiś poprawkami w tym kontekście.

A wszystko zaczęło się od sławnego już artykułu Rails is a ghetto. Czyżby Zed Shaw kolejny raz, tym razem może w całkiem niezamierzony sposób, przyczynił się do poprawy sytuacji?

Mam nadzieję, że prace nad ułatwieniem deploymentu railsów niedługo się zaczną. Oznaczałoby to też poprawę uruchamiania skryptów rubiego w taki sam sposób jak skryptów php – wrzucam na serwer, wpisuję adres, dostaję wynik.

Poczekamy, zobaczymy, może coś się ruszy :)

Posted in  | Tags , , , ,  | no comments | no trackbacks

Otwarte klasy w Rubim i ich praktyczne wykorzystanie

Posted by Piotr Sarnacki Tue, 08 Jan 2008 10:06:00 GMT

Ruby posiada bardzo fajną właściwość nazwaną otwartymi klasami. Znaczy to tyle, że jeżeli nikt nie zamroził danej klasy/metody, można w dowolnym miejscu w kodzie nadpisać ją klasę, metodą, dodać nowe metody. Można tak nadpisać nawet klasy wbudowane w język!

Na przykład taki kod (och jak ja lubię ten przykład):
class Fixnum
  alias old_plus +

  def +(other)
    (self.old_plus other) % 7
  end
end

4+4 #=> 1

4+4=1 ? nie tego się spodziewaliśmy. A właściwie nie tego się spodziewali ludzie, którzy nie rozumieją jeszcze powyższego przykładu (celowo dodałem słowo jeszcze, będę na tyle cywilizowanym i miłym człowiekiem, że spróbuję wytłumaczyć o co chodzi).

Liczby całkowite są klasy Fixnum. Powyższy kod modyfikuje tą klasę. Najpierw tworzony jest alias dla metody +, żeby można było jej później używać, a następnie owa metoda zostaje nadpisana w taki sposób, żeby zwracała wynik dodawania modulo7

Do napisania tego artykułu natchnął mnie Daniel Owsiański pisząc o zjawisku roboczo nazwanym version lock-in. Daniel ma oczywiście dużo racji i moja paranoja, o której pisałem u niego w komentarzach jest objawem przewrażliwienia mojej mózgoczaszki w pewnych kwestiach. Są jednak wypadki, w których naprawdę warto zachować zgodność z nowymi wersjami. Pomaga tutaj powyższa właściwość języka Ruby. Pisał o tym kiedyś autor bloga Err the Blog w kontekście rozszerzania możliwości pluginów.

Często w rozmowach o Ruby on Rails na różnych listach dyskusyjnych można usłyszeć, że gdy chcemy coś zmienić w danej metodzie, najlepiej przekopiować kod metody, nadpisać ją, zmienić to co trzeba i voilla. Ale nie tędy droga panie i panowie :)

Wyglądałoby to mniej więcej tak. Chcemy na przykład nadpisać metodę find. Wchodzimy na Rails API, znajdujemy ActiveRecord::Base#find, wklejamy kod w modelu i zmieniamy:
#w modelu:
def self.find(*args)
  #jakiś zmodyfikowany kod finda
end

Jakie minusy ma takie podejście? Gdy mamy zainstalowaną dużą liczbę pluginów nigdy nie wiadomo czy któryś z nich nie nadpisuje już metody find i wtedy nadpisując ją stracimy funkcjonalność dodaną przez plugin. Smuteczek. Poza tym gdy zmieni się kod metody find w samym frameworku również u nas będziemy musieli go zmienić.

Jak to zrobić Ruby Way™? Przypuśćmy, że chcemy się popastwić nad wspomnianą metodą find:

#w modelu:

# metoda find jest metodą klasy
class << self
  alias :old_find :find

  def find(*args)
    args[1] ||= {}
    args[1][:conditions] ||= {} 
    args[1][:conditions] = [args[1][:conditions]] if args[1][:conditions].is_a?(String)
    case args[1][:conditions]
      when Hash: 
        args[1][:conditions].merge!(:deleted => false)
      when Array:
        if args[1][:conditions][0].strip.blank?
          args[1][:conditions][0] = "deleted = ?" 
        else
          args[1][:conditions][0] = ["(#{args[1][:conditions][0]})", "deleted = ?"].join(' AND ')
        end
        args[1][:conditions] << false       
    end
    old_find(*args)
  end
end

Powyższy kod dodaje do conditions warunek “deleted = false”, po czym wywołuje metodę find z tak zmodyfikowanymi argumentami. Czasami trzeba jednak namieszać coś w kodzie metody. Można wtedy dodać dodatkowy argument. Następnie dajemy ifa – jeżeli argument zwraca true wykonujemy zmodyfikowany kod, a jeżeli nie, wykonujemy kod oryginalnej metody.

Dzięki takiemu podejściu możemy w miarę łatwo upgradować pluginy i Railsy bez większego stresu :)

Posted in  | Tags , , , , ,  | no comments | no trackbacks

Older posts: 1 2