Wygenerujmy sobie proste rusztowanie (ang. scaffolding):
script/generate scaffold user first_name:string last_name:string
Następnie dodajmy kilka walidacji dla naszego modelu:
class User < ActiveRecord::Base validates_presence_of :first_name, :last_name end
Odpalamy migracje i wchodzimy na:
http://localhost:3000/users/new
Nie wypełniając żadnego pola klikamy przycisk Create. Niby wszystko gra, dostajemy informacje o błędach walidacji i takie tam. Jednak jeżeli przyjrzymy się dokładnie, a w szczególności adresowi URL to widzimy:
http://localhost:3000/users
Adres się zmienił! Ale przecież nie klikaliśmy żadnego linku! Ba nawet w kodzie nie ma w tym miejscu przekierowania:
def create
@user = User.new(params[:user])
respond_to do |format|
if @user.save
# ...
else
format.html { render :action => "new" }
# ...
end
end
end
Dlaczego zatem adres się zmienił?
Aby zrozumieć dlaczego tak się stało musimy przeanalizować kod HTML wygenerowany dla formularza. Wchodzimy jeszcze raz na formularz, wyświetlamy źródło strony i szukamy znacznika <form>:
<form action="/users" class="new_user" id="new_user" method="post">
<!-- Pola formularza -->
</form>
Widać, że posiada on atrybut action z wartością /users. Oznacza to, że przeglądarka wygeneruje żądanie POST pod adres http://localhost:3000/users. Rzućmy okiem na nasz routing:
$ rake routes | grep create
POST /users(.:format) {:controller=>"users", :action=>"create"}
Widzimy, że faktycznie spodziewamy się takiego żądania pod ten adres, a ponieważ metoda render w odróżnieniu od redirect_to nie generuje przekierowania w przeglądarce ustawiony zostaje adres taki jak przy żądaniu POST. Wszystko zatem działa poprawnie, bo tak zostało skonfigurowane. Czy to jest błąd? To pewnie zależy od aplikacji, w każdym razie niezbyt profesjonalne jest kiedy adres URL zmienia się bez wyraźnego powodu. Oto kilka sposobów jak sobie można z tym poradzić.
Flash to jest tymczasowy bufor w Rails, w którym możemy przechować dane pomiędzy kolejnymi żądaniami HTTP (w jednym coś wrzucamy a w następnym wyciągamy). Najczęściej flasha używa się do przekazywania komunikatów, w buforze może być umieszczany jednakże nie tylko tekst.
Zatem możemy nasze wywołanie metody render, które renderuje tylko wybrany szablon, a nie generuje przekierowania zamienić na redirect_to. Ponieważ kolejne żądanie spowoduje utworzenie nowego obiektu kontrolera potrzebujemy sposobu przechowania naszego utworzonego obiektu user aby nie zgubić błędów walidacji. Możemy się posłużyć właśnie buforem flash:
class UsersController < ApplicationController
def new
@user = flash[:user] || User.new
respond_to do |format|
format.html # new.html.erb
end
end
def create
@user = User.new(params[:user])
respond_to do |format|
if @user.save
# ...
else
flash[:user] = @user
format.html { redirect_to :action => "new" }
end
end
end
# Reszta metod ...
end
To podejście jest proste i działa, jednakże ma swoje wady. Bufor flash jest trzymany w sesji co oznacza, że obiekty, które tam wrzucamy muszą dać się serializować. Do tego w przypadku, gdy sesja jest trzymana w ciasteczkach mamy ograniczenie 4KB na dane sesji. Generalnie wrzucanie dużych obiektów (albo i w ogóle obiektów) do sesji to zły pomysł, raczej posługujemy się identyfikatorami. Dodatkowo w tym podejściu generujemy dodatkowe żądanie do serwera (co generalnie nie jest nam aż tak bardzo potrzebne).
Tak więc to podejście może być jedynie szybkim, tymczasowym rozwiązaniem, jednak aby w pełni sobie poradzić z problemem, trzeba się nieco bardziej wysilić.
Innym rozwiązaniem może być modyfikacja routingu.
W tym momencie nasz plik routes.rb definiuje standardowy routing, dla kontrolera :users:
map.resources :users
Możemy wyrzucić routing na metodę create i dodać ręcznie routing kierujący żądania POST wysłane na adres /users/new do tej metody:
map.resources :users, :except => [ :create ]
map.connect 'users/new',
:conditions => { :method => :post },
:controller => :users,
:action => :create
Musimy jeszcze zmienić adres w formularzu:
<% form_for(@user, :url => new_user_path) do |f| %>
<!-- Pola formularza -->
<% end %>
(Podobnie należałoby postąpić z metodą update.)
Podejście to działa i jest pozbawione wad poprzedniej metody. Wymaga nieco więcej wysiłku, ale nie generujemy dodatkowych przekierowań ani nie wrzucamy niczego do sesji. Problemem jest jedynie na stałe wprowadzony adres users/new w routingu.
Możemy również pominąć deklarację routingu na metodę create i w metodzie new rozpatrywać rodzaj żądania. Ten sposób jest jednak mało deklaratywny i nie przynosi niczego szczególnie dobrego.
Routing oparty na zasobach, mimo iż w prosty sposób pozwala nam na tworzenie aplikacji RESTful-owych nie jest pozbawiony wad. Jedną z nich jest zamiana adresu URL dla formularza, nawet jeżeli pozostajemy wciąż na tym samym ekranie. Nie w każdej aplikacji będzie to błąd, jednak nie świadczy to zbyt dobrze o twórcy aplikacji.
Jest kilka sposobów na radzenie sobie z tym problemem. Możemy zamiast renderowania szablonu wykonać przekierowanie na odpowiedni adres URL przechowując zwalidowany obiekt w buforze flash. Możemy także ręcznie zmodyfikować routing w pliku routes.rb. Innym sposobem, nie opisanym tutaj. może być wykorzystanie AJAX-u do walidacji. Podejście to wymaga najwięcej pracy, ale jednocześnie daje aplikację bardziej responsywną i przyjazną użytkownikowi.
http://michalorman.pl/blog/2010/03/zmieniajacy-sie-url-po-bledach-walidacji-w-rails/
Full stack software developer, konsultant IT.
Niekwestionowany full stack developer, architekt rozwiązań IT, project manager, entuzjasta Agile z biznesowym zacięciem, który lubi robić rzeczy do końca. Prawdziwy człowiek orkiestra, którego największymi zaletami są nieustanne dążenie do perfekcji, produktywność, dbanie o najwyższą jakość wyprodukowanego oprogramowania. Obecnie programuje w Ruby on Rails i JavaScript, ale do listy znanych mu technologii bez wątpienia można dopisać Java/J2EE. Tworzył aplikacje mobilne na platformę Android, systemy wbudowane w C/C++. Zapalony wyznawca zasad SOLID lubiący proste rozwiązania oraz czysty kod. Do swojego portfolio technologicznego również może dodać Unix/Linux, VIM oraz Git.
Comments