The service provided by Consileon was professional and comprehensive with a very good understanding of our needs and constrains.

Wolfgang Hafenmayer, Managing partner, LGT Venture Philanthropy

Technical quality of staff offered, capability of performing various project roles, as well as motivation and dedication to the project (... [...]

dr Walter Benzing, Head of development B2O, net mobile AG

Technical quality of staff offered, capability of performing roles of developers, lead developers, trainers, team leaders, architects as wel [...]

Karl Lohmann, Itellium Systems & Services GmbH

Firma Consileon Polska jest niezawodnym i godnym zaufania partnerem w biznesie, realizującym usługi z należytą starannością (...)

Waldemar Ściesiek, Dyrektor zarządzający IT, Polski Bank

The team was always highly motivated and professional in every aspect to perform on critical needs of our startup environment.

Denis Benic, Founder of Ink Labs

Zmieniający się URL po błędach walidacji w Rails

Category: Other Tags: ,

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ł?

Źródło problemu

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

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

Routing

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.

Podsumowanie

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/


Michał Orman

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 progra­muje 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.

michalorman.com


Tags: ,

Comments

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *

Trwa ładowanie