Tutorial: recursos anidados con REST y Rails 2

Aunque el tema de REST y ActiveResource está disponible en Rails 1.2, hasta ahora ha sido un tema que he ido posponiendo en mi lista de cosas por aprender.

La introducción de REST por defecto en la generación de scaffolds de Rails 2 ha cambiado las cosas, y me ha hecho ver que no puedo dejar para más adelante el actualizarme a la filosofía REST.

He estado haciendo algunas pruebas para aclarar las ideas, así que aquí os dejo los códigos para que podáis echarle un vistazo, aprender conmigo y dejarme comentarios… seguro que he pasado por alto algunas cosas.

El objetivo de este tutorial es practicar los recursos anidados. Queremos construir una aplicación típica, un blog, pero en este caso, multiusuario. Tendremos N usuarios, cada uno de los cuales tiene M posts, y cada post tiene Z comentarios. Lo que buscamos es poder definir rutas REST anidadas, de manera que cada recurso (usuario, post, comentario) sólo tenga sentido dentro del contexto de su padre. En concreto, queremos rutas como estas:

# Listado de usuarios
/users

# Un usuario determinado
/users/:user_id

# Posts de un usuario determinado
/users/:user_id/posts

# Un post determinado de un usuario determinado
/users/:user_id/posts/:post_id

# Comentarios de un post y usuario determinados
/users/:user_id/posts/:post_id/comments

# Comentario determinado de un post y usuario determinados
/users/:user_id/posts/:post_id/comments/:comment_id

Como se ve, son rutas muy limpias y autoexplicativas.

Os explico en rasgos generales lo que hay que hacer para conseguir esto. Tras crear nuestra aplicación y la base de datos, generaremos los 3 scaffolds para controlar nuestros 3 recursos (users, posts y comments):

rails blog
cd blog
rake db:create
script/generate scaffold User name:string
script/generate scaffold Post title:string body:text user_id:integer
script/generate scaffold Comment body:text post_id:integer
rake db:migrate

Los scaffolds generarán los modelos y migraciones, controladores y vistas, etc. Además, introducirán rutas REST por defecto para cada modelo, pero las tenemos que cambiar porque no queremos que sean independientes, sino anidadas. Esto es, no queremos gestionar un post fuera del contexto de su usuario, ni un comentario fuera del contexto de su post y del usuario de este post.

Para ello, edita config/routes.rb, y elimina estas 3 líneas:

map.resources :comments
map.resources :posts
map.resources :users

Y pon en su lugar esta ruta anidada:

map.resources :users do |user|
   user.resources :posts do |post|
     post.resources :comments
   end
 end

Ahora, edita los modelos e introduce la relación entre ellos como de costumbre en Rails:

class User < ActiveRecord::Base
   has_many :posts, :dependent => :destroy
 end

class Post < ActiveRecord::Base
   belongs_to :user
   has_many :comments, :dependent => :destroy
 end

class Comment < ActiveRecord::Base
   belongs_to :post
end

Ahora, tendremos que modificar los controladores y vistas, porque fueron generados sin tener en cuenta que estarían anidados, así que hemos de actualizarlos. El controlador de usuarios (user_controller.rb) no necesita cambios porque es independiente de los de posts y comentarios.

Donde sí necesitamos cambios es en el controlador de posts. En primer lugar, necesitamos asegurar que un post sólo tiene sentido dentro del contexto de un usuario determinado, que se habrá pasado en la URL. Así que usaremos un filtro before_filter para que, antes de realizar cualquier acción, coja este usuario:

  before_filter(:get_user)

El método get_user lo definiremos así, dentro del apartado de métodos privados ya que no se usa más que desde dentro de este controlador:

private

def get_user
   @user = User.find(params[:user_id])
end

Ahora, modificaremos el resto de los métodos para que no se busquen los posts en general, sino dentro del contexto del usuario. Por ejemplo, en el método index, que lista los posts, en lugar de mostrarlos todos con:

@posts = Post.find(:all)

Mostraremos sólo los de ese usuario:

@posts = @user.posts.find(:all)

Modificaremos de manera similar el resto de acciones, para buscar el post a mostrar, crear, modificar o eliminar dentro de la colección de posts del usuario. En lugar de:

@post = Post.find(params[:id])

Usaremos:

@post = @user.posts.find(params[:id])

Esto nos añade seguridad ya que por ejemplo para editar un post, hay que indicar no sólo su id sino también su user_id, y si no son válidos, no lo encontrará.

Dentro del controlador también hay que cambiar las redirecciones, ya que por ejemplo tras crear un post, para redirigir al “show” de este post venía así recién creado el scaffold:

redirect_to(@post)

Ahora, como se trata de un recurso anidado dentro del usuario, hay que indicarle la ruta completa así:

redirect_to(user_post_url(@user, @post))

Echa un vistazo a los códigos de ejemplo que adjunto en este tutorial para ver el resto de modificaciones en este controlador.

El controlador de comentarios (comment_controller.rb) necesitará modificaciones similares, pero esta vez además del usuario hemos de recuperar el post, para tener el contexto completo. Es muy similar al ejemplo anterior, primero usamos los siguientes before_filter:

before_filter(:get_user)
before_filter(:get_post)

Y los definimos así:

private

def get_user
   @user = User.find(params[:user_id])
end

def get_post
   @post = User.find(params[:user_id]).posts.find(params[:post_id])
end

Fíjate en que podría haber cogido el post directamente pero para mayor seguridad lo estoy cogiendo a través del usuario.

Las redirecciones también serán similares. Por ejemplo, tras crear un comentario y redirigir al comentario, sería así:

redirect_to(user_post_comment_url(@user, @post, @comment))

Como ves, es similar pero indicando el usuario y post y comentario a que quieres redirigir.

También será necesario que edites todas las vistas de posts y comments, ya que los helpers generados por el scaffold no te valen, y has de indicar el contexto en todos. Por ejemplo, el enlace para crear un nuevo comentario será así ahora:

link_to 'New comment', new_user_post_comment_path(@user, @post)

Eso es todo por mi parte… Os dejo a continuación los códigos de este ejemplo para que podáis ver cómo he construido el resto de rutas con la ayuda de los helpers de Rails 2.0.2.

Códigos del blog rest con rutas anidadas

Seguro que he cometido unas cuantas correcciones, por lo que os agradecería que me lo indicarais con vuestros comentarios.