Ruby on Rails, parte 16: Relaciones en los modelos

1 Jul

Como ya dijimos en posteos pasados cada modelo representa una tabla en una base de datos, por tanto así como las tablas tienen relaciones entre sí también los modelos.

Seguramente debés haber trabajado alguna vez con una base de datos, y si no es así tal vez debas saber que las tablas tienen tres tipos de relaciones. Relaciones de 1 a 1, de 1 a n (n significa muchos) y de n a n.

Relaciones de 1 a 1

Estas se dan cuando un registro está ligado a otro. Por ejemplo, nosotros podemos tener un modelo llamado Usuario y un modelo llamado Cuenta. Cada usuario tendrá una cuenta y esa cuenta le pertenecerá a un usuario.

Relaciones de 1 a n

Son las relaciones más comunes. En este caso un registro puede tener relación con otros, pero cada uno de esos otros le pertenecerán sólo a ese registro. Por ejemplo, siguiendo con el modelo Usuario y un modelo Publicacion, cada usuario puede tener varias publicaciones, pero cada publicación le pertenecerá a un usuario.

Relaciones de n a n

Estas relaciones se dan cuando un registro puede tener relaciones con otros registros, pero a su vez estos registros además de tener relación con el primero pueden estar vinculados con otros. Por ejemplo, suponiendo que seguimos con el modelo Usuario, nosotros queremos saber qué bandas de música le gustan a cada usuario, así que también tendremos un modelo Banda. Entonces en ese caso cada a un usuario le pueden gustar varias bandas de música, pero estas bandas también le gustarán a varios usuarios. Las relaciones de n a n siempre tendrán una tabla intermedia y por ende también un modelo.

Siguiendo esta lógica esto quedaría así:

Modelo Usuario:

class Usuario < ActiveRecord::Base
   has_one :cuenta
   has_many :publicacions
   has_and_belongs_to_many :bandas
end

Notar que cuando se habla de muchos como has_manyhas_and_belongs_to_many se utiliza plural, o en realidad se le agrega un “s”, recordar que RoR está escrito en inglés.

Modelo Cuenta:

class Cuenta < ActiveRecord::Base
   belongs_to :usuario
end

Modelo Publicacion:

class Publicacion < ActiveRecord::Base
   belongs_to :usuario
end

Modelo Banda:

class Banda < ActiveRecord::Base
   has_and_belongs_to_many :usuarios
end

Modelo UsuarioBanda (modelo que une Usuario con Banda):

class UsuarioBanda < ActiveRecord::Base
   belongs_to :usuario
   belongs_to :banda
end

 

Bueno, ahora que ya tenemos una idea de la unión de modelos vamos a seguir con nuestro proyecto organizador, creando las funcionalidades del modelo Comentario, modelo que está unido a Tarea. La relación entre ambos es de 1 a n, osea una tarea puede tener muchos comentarios. Así que crearemos las acciones para mostrar, insertar, modificar y eliminar comentarios.

Vamos dentro de nuestro raíz a config -> routes.rb y buscamos dentro de este archivo la línea que habíamos creado antes:

resources :tareas

Haremos una pequeña modificación de esta forma:

resources :tareas do
   resources :comentarios
end

De esta manera podemos como agrupar los comentarios dentro de las tareas, por tanto cuando tengamos que guardar nuevos comentarios también tendremos en cuenta la tarea a la que pertenece el mismo.

En primer lugar iremos al modelo Tarea dentro de app -> models -> tarea.rb y vamos a agregar dentro del modelo la siguiente línea:

has_many :comentarios

Ahora vamos al modelo Comentario, en la misma carpeta obviamente y editamos el archivo tarea.rb. Buscamos la línea:

belongs_to :tarea

Si está, la dejamos, si no está la agregamos. También buscamos que exista la línea:

attr_accessible :mensaje

Lo mismo, si está la dejamos y sino la agregamos y la misma acción para:

attr_accessible :tarea

Esta última probablemente no esté así que tendremos que agregarla.

Y finalmente las validaciones del modelo, que sólo tendrá el atributo mensaje:

validates :mensaje, :presence => {:message => "Usted debe ingresar un comentario"}, length: {minimum: 2, maximum: 4000, :message => "El comentario debe tener entre 2 y 4000 caracteres"}

Bien ahora con estos últimos cambios le indicamos a nuestros modelos que una tarea tiene ‘muchos comentarios’ (has_many) y que los atributos del modelo Comentario:mensaje y :tarea, deben ser accesibles para poder insertar y modificar comentarios. En el caso de :mensaje, este atributo será un string, pero el atributo :tarea será un objeto, un objeto de la clase Tarea.

Ahora vamos desde la consola crear el controlador comentarios con cinco acciones: new, save, edit, update y delete:

rails g controller comentarios new create edit update destroy

A diferencia del controlador tareas, no crearemos las acciones index y show ya que la lógica de nuestra aplicación sólo requiere que se muestren los comentarios dentro de las acciones de las tareas.

Ahora vamos a dentro de app -> views -> comentarios dentro vamos a eliminar los archivos: destroy.html.erbcreate.html.erbupdate.html.erb, ya que estas acciones no necesitarán una salida html. Y dentro de este directorio crearemos un nuevo archivo parcial llamado: _formulario_guardar_comentarios.html.erb. Todo este proceso es similar al que hicimos para crear las acciones de las tareas, con mínimas diferencias.

Bueno, antes de modificar los archivos de la vista comentarios vamos a hacer unas cosillas en tareas. Vamos primero a nuestro controlador tareas y modificamos el método show de la última vez que estaba así:

def show
   @tarea = Tarea.find(params[:id]);
end

Por esto:

def show
   @tarea = Tarea.find(params[:id]);
   @comentarios = Comentario.select("id, mensaje").where(:tarea_id => params[:id]);
end

Además de la tarea, esta acción también mostrará los comentarios de la misma.

Ahora vamos a la vista show.html.erb dentro de la carpeta tareas y modificamos lo de la última vez:

<h2> <%= @tarea.titulo %> </h2>
<div>
   <%= simple_format @tarea.descripcion %>
</div>
<p> <%= link_to "Volver", tareas_path %> </p>

Por esto:

<h2> <%= @tarea.titulo %> </h2>
<div> 
   <%= simple_format @tarea.descripcion %>
</div>
<p> <%= link_to "Agregar un comentario", new_tarea_comentario_path(@tarea) %> | <%= link_to "Volver", tareas_path %> </p>
<hr />
<ul style="list-style: none; padding: 0px;">
   <% @comentarios.each do |comentario| %>
   <li>
      <div> <%= simple_format comentario.mensaje %> </div>
      <p> 
         <%= link_to "Editar", edit_tarea_comentario_path(@tarea, comentario) %>
         | 
         <%= link_to "Eliminar", tarea_comentario_path(@tarea, comentario), :confirm => "Seguro desea eliminar este comentario?", :method => :delete %> 
         </p>
         <hr />
   </li>
 <% end %>
</ul>

Ok, vamos a analizar algunos cambios. En primer lugar le agregamos un link para agregar nuevos comentarios :

<%= link_to "Agregar un comentario", new_tarea_comentario_path(@tarea) %>

Sin embargo debemos aclararle a la acción primero sobre qué tarea se trata, por eso es new_tarea_comentario_path y no new_comentario_path. Y debemos pasar como parámetro la tarea en dónde se va a insertar el comentario, rails generará una url con el id del comentario.

También creamos un each para recorrer los comentarios de la tarea que han sido guardados:

<ul style="list-style: none; padding: 0px;">
   <% @comentarios.each do |comentario| %>
   <li>
      <div> <%= simple_format comentario.mensaje %> </div>
      <p> 
         <%= link_to "Editar", edit_tarea_comentario_path(@tarea, comentario) %>
         | 
         <%= link_to "Eliminar", tarea_comentario_path(@tarea, comentario), :confirm => "Seguro desea eliminar este comentario?", :method => :delete %> 
         </p>
         <hr />
   </li>
 <% end %>
</ul>

También generamos dos link. Uno para modificar:

<%= link_to "Editar", edit_tarea_comentario_path(@tarea, comentario) %>

En este caso, como es modificar y no insertar, debemos cambiar new_tarea_comentario_path por edit_tarea_comentario_path, y recibirá dos parámetros, la tarea y el comentario que vamos a modificar.

Y el segundo link para eliminar:

<%= link_to "Eliminar", tarea_comentario_path(@tarea, comentario), :confirm => "Seguro desea eliminar este comentario?", :method => :delete %>

Bueno, ahora continuemos con las acciones de comentarios, así que abrimos el controlador comentarios.rb y las vistas: _formulario_guardar_comentarios.html.erb, new.html.erb y edit.html.erb.

Comenzamos con la acción new. Entonces el método quedará así:

def new
   @tarea = Tarea.find(params[:tarea_id]);
   @comentario = Comentario.new();
end

Tendremos dos objetos, el primero es un objeto tarea, recuperaremos la tarea con el método .find(), la tarea a la que le corresponderá el nuevo comentario. Y un objeto comentario nuevo.

Ahora vamos a editar la vista _formulario_guardar_comentarios.html.erb de esta forma:

<%= form_for([@tarea, @comentario]) do |f| %>
   <% if @comentario.errors.any? %>
      <ul>
         <% @comentario.errors.each do |atributo, mensaje| %>
         <li> <%= mensaje %> </li>
         <% end %>
      </ul>
   <% end %>
   <%= f.text_area :mensaje, {:value => @mensaje} %>
   <br />
   <%= f.submit "Enviar" %> <%= link_to "Volver", @tarea %>
<% end %>

Bueno, por empezar el helper form_for va recibir como parámetro un array con dos objetos, el objeto tarea y el objeto comentario. Esto cambia de cuando tuvimos que crear un formulario para guardar tareas, ya que las mismas no están ligadas con otros registros al momento de ser creadas. Pero en el caso de los comentarios sí, al crear un comentario debemos indicar a qué tarea pertenece.

Luego también tuvimos que crear una lista para  verificar si hay errores de validación:

<% if @comentario.errors.any? %>
   <ul>
      <% @comentario.errors.each do |atributo, mensaje| %>
      <li> <%= mensaje %> </li>
      <% end %>
   </ul>
<% end %>

En este caso el único error posible es el de no ingresar el comentario.

Ahora vamos la vista new.html.erb e incluimos el parcial del formulario:

<h2> Agregar comentario </h2>
<%= render :partial => "formulario_guardar_comentarios" %>

Ahora sólo resta editar la acción create para que inserte el comentario. Así que vamos al método create del controlador comentarios que debe quedar así:

def create
   @mensaje = params[:comentario][:mensaje];
   @tarea = Tarea.find(params[:tarea_id]);
   @comentario = Comentario.new({
      :mensaje => @mensaje,
      :tarea => @tarea
   });
   if @comentario.save()
      redirect_to @tarea, :notice => "El comentario ha sido insertado";
   else
      render "new";
   end
end

El código es muy parecido a del otro método create, al del controlador tareas. Guardamos el mensaje que envió el usuario en una variable, luego creamos el objeto tarea, y el objeto comentario con dos atributos, el mensaje y la tarea. Y luego con un if verificamos si se ha podido guardar el comentario. Si se ha podido insertar correctamente redireccionamos a la tarea, y sino volvemos a cargar la acción new.

Ahora haremos lo mismo para las acciones de modificar el comentario. El método edit quedará así:

def edit
   @tarea = Tarea.find(params[:tarea_id]);
   @comentario = Comentario.find(params[:id]);
   @mensaje = @comentario.mensaje;
end

Y la vista edit:

<h2> Modificar comentario </h2>
<%= render :partial => "formulario_guardar_comentarios" %>

Ahora sólo falta la acción que modificará el comentario. Así que cambiamos el método update por esto:

def update
   @mensaje = params[:comentario][:mensaje];
   @tarea = Tarea.find(params[:tarea_id]);
   @comentario = Comentario.find(params[:id]);
   @comentario.mensaje = @mensaje;
   if @comentario.save()
      redirect_to @tarea, :notice => "El comentario ha sido modificado";
   else
      render "edit";
   end
end

Y finalmente el método destroy que quedará así:

def destroy
   @tarea = Tarea.find(params[:tarea_id]);
   @comentario = Comentario.find(params[:id]);
   if @comentario.destroy()
      redirect_to @tarea, :notice => "El comentario ha sido eliminado";
   else
      redirect_to @tarea, :notice => "El comentario no se ha podido eliminar";
   end
end

 

Unir dos modelos en una consulta

Hasta ahora todas las consultas que hemos realizado son consultas como buscar una tarea por su id, una lista de tareas o una lista de comentarios; sin embargo también podríamos hacer una consulta uniendo dos tablas, un join.

Para eso iremos al controlador tareas y editaremos el método index que tenía este aspecto:

def index
   @tareas = Tarea.select("id, titulo, descripcion").where(:activo => true).order("id DESC");
end

Modificando el método por esto:

def index
   @tareas = Tarea.select("id, titulo, descripcion").where(:activo => true).order("id DESC");
   @ultimos_comentarios = Tarea.select("tareas.id, tareas.titulo, comentarios.mensaje").joins(:comentarios).order("comentarios.id DESC").limit(10);
end

Además de traernos la lista de las tareas nos devolverá una lista que contendrá el id de la tarea, la descripción y el mensaje de un comentario, ordenados por los últimos diez comentarios. En este caso para unir dos modelos debemos utilizar el método .joins().

Ahora vamos a la vista index.html.erb de las tareas y agregamos debajo de nuestro código esto:

<h2> Últimos comentarios </h2>
<ul style="list-style: none; padding: 0px;">
   <% @ultimos_comentarios.each do |comentario| %>
      <li>
         <p> <%= comentario.mensaje[0..50] %>... </p>
         <p> Comentario en <%= link_to comentario.titulo, tarea_path(comentario.id) %> </p>
         <hr />
      </li>
   <% end %>
</ul>

Bueno, con esto terminamos.

Saludos a todos!

Anterior: Ruby on Rails, parte 15: Validaciones

Siguiente: Ruby on Rails, parte 17: Scaffolding

Redes sociables