Rotas aninhadas para definir um contexto no Rails


English Version (Google Translate)

Em Rails temos um arquivo (até o atual Rails 2.2 o arquivo é routes.rb mas no Rails 2.3 em diante poderemos ter outros) onde podemos definir as rotas de nosso aplicativo, customizando cada url de nosso aplicativo ( semelhante ao resultado obtido com o mod_rewrite que o pessoal costuma usar em sites em PHP )

Por que uma url deve refletir seu conteúdo?

Por vários motivos como serem lembradas mais fácil, dizer exatamente do que um link se trata ates do usuário tentar acessar, pontuar melhor em sistemas de busca e muitas outras vantagens…

Rotas aninhadas e parâmetros através de rotas

E muita gente do mundo Rails já deve ter visto este post no blog de Jamis Bucks.

Basicamente, Jamis defende que não devemos aninhar rotas por mais de um nível no Rails. Em seu post, sua solução tenta sempre manter uma url mais limpa. Concordo com Jamis ( quem sou para descordar ) que devemos tentar sempre manter uma url menor.

Mas as rotas em Rails trazem algumas vantagens além de apenas refletir na url, através das rotas é possível acessar parâmetros, como por exemplo:

http://www.areacriacoes.com.br/users/1/posts/2

Na url acima eu consigo ter acesso a dois parâmetros, sem trabalho algum, apenas pela url… isso sem passar parâmetros via get na url ou armazenar nada na session. Então temos:

params[:user_id] # retorna => 1
params[:id]         # retorna => 2

E para gerar o link acima dentro do meu aplicativo seria algo mais ou menos assim:


link_to "post 2 do Daniel", user_post_path(params[:user_id] ,params[:id])

Então além de ajudar a termos uma url mais limpa e inteligente, nossas rotas também servem para enviar parâmetros sem poluir a url com parâmetros e sem precisar setar nada na session.

Isso torna nossas url perfeitas para armazenar dados relevantes a associações em banco de dados. No meu exemplo acima post pertence a um usuário. E desta forma posso encontrar este usuário em um método before_filter no meu controller.

O problema

Mas ai que começa o problema. Considerem o exemplo onde usuário possui imóveis e imóveis possui inquilinos. Então a primeira coisa que pensamos é aninhar nossas rotas em 3 níveis, teriamos o código abaixo no nosso routes.rb:

1
2
3
4

map.resources :users do |user|
  user.resources :properties, :has_many=> :tenants
end

Então teriamos as urls:

/user/1
/user/1/properties
/user/1/properties/1/tenants

Apesar de ser bem explicativa, a nossa 3º url já começa a ficar grande, mas na minha opinião este não é o maior problema… o grande problema é poluição no nosso código. Se quisermos usar a 3º url teriamos algo mais ou menos assim:


link_to "inquilinos do imóvel 1", user_property_tenants_path(@user,@property)

E se quisermos exibir a action show de um único inquilino teremos algo mais feio ainda:

link_to "inquilino 1 do imóvel 1", user_property_tenant_path(@user,@property,@tenant)

Absolutamente terrível. Agora temos uma url grande e ainda temos que passar 3 parâmetros…

A solução de Jamis Bucks

Então se recorrermos a solução do Jamis para não aninhar mais de dois níveis teremos o seguinte em nosso routes:

Definindo que temos contas que possuem pessoas que possuem anotações que possuem comentários… tudo sempre aninhado em no máximo dois níveis, removendo aquele comportamento terrível que citamos acima. Mas o problema é que neste caso teremos um desperdício de rotas.

Se rodarmos o nosso comando rake routes no terminal teremos o seguinte retorno:

Acima mostrei só o trecho respectivo a contas e pessoas… reparem que o método do Jamis resulta tanto na url /people quanto também é possívle usar /accounts/1/people … então temos people aninhado e não aninhado.

Mas em muitos casos eu não quero ter acesso a people sem estar aninhado á accounts… e seria díficil por exemplo ficar definindo métodos que agiriam diferente dependendo do parâmetro se está na url ou na session, então acabariamos não usando os parâmetros da url ( que na minha opinião é uma grande mão na roda).

Então eu prefiro não ter as rotas que possam ser acessadas sem aninhamento… mas se reparamos em nossas relações não faz sentido aninhar muitos níveis… por exemplo, contas possuem pessoas que possuem anotações que possuem comentários. Então comentários devem estar aninhados apenas em anotações e anotações apenas em pessoas e pessoas apenas em contas.

Então, no meu ponto de vista acho mais correto sempre termos as rotas aninhas em seus pais em uma associação belongs_to.

Desta forma é impossível acessar people sem estar dentro de account … e não faz muito sentido ser possível acessar se você estiver usando as urls para passar parâmetros. Mas como fariamos para ter estas seguintes rotas no exemplo do Jamis?

Rotas definindo associações, e sem mais de dois níveis

O que resultaria em:

Desta forma, por exemplo, podemos definir um before_filter em nosso controller comments para pré carregar o objeto associado pai, note, como abaixo:

1
2
3
4
5
6
7
8

  def load_note
    @note = Note.find_by_id params[:note_id]
    if @note.nil?
      flash[:notice] = "Registro não encontrado"
      redirect_to root_path
    end
  end

E então em métodos que precisem buscar um comentário em especial podemos tirar o uso de association_proxies:

1
2
3
4
5

  # url => notes/1/properties/1
  def show
    @comment = @note.comments.find(params[:id])
  end

Conclusão

A única desvatagem que vejo é que desta forma voltamos a uma sintaxe que não utiliza blocos nem has_many para aninhar rotas. Mas não vejo isso como um grande problema.

O ideal seria que tivessemos um método que fosse possível aninhar rotas sem criar a rota pai, mas como não temos isso no Rails, o método acima resolve os problemas.

Se você conhece alguma outra solução, por favor compartilhe abaixo.


2 Comentários to “Rotas aninhadas para definir um contexto no Rails”

Rafael diz:

Salve,

Recentemente o Ryan Bates do Railscasts fez um screencast falando sobre esse assunto, mostrando uma nova funcionalidade do Rails 2.2.2 onde podemos definir rotas aninhadas apenas para actions específicas utilizando o parâmetro :only, evitando assim a grande quantidade de sub-rotas geradas. Ele discute bem o problema e inclui o artigo do Jamis nos links de referência.

http://railscasts.com/episodes/139-nested-resources

Recomendo.

Abraço, Rafael.


Daniel Lopes diz:

Sim, este screencast foi um dos motivos que me fez criar este post. Mas vale lembrar que o only e o except são para definir qual url deve estar disponível ( ou não ) de acordo com cada resource… e se você fizer da forma como o jamis bucks mostrou e não quiser ter as rotas sem aninhamento de um nível, você teria que usar o only para quarto rotas ( o que acho meio feio ).

Outra solução seria aninhar e definir path_prefix e name_prefix de cada item aninhado para nil.


Comentário