CRUD en Rails

Para ver como funcionan los formularios para hacer CRUD (Create, Read, Update, Delete) en Rails vamos a hacer una aplicación muy sencilla que servirá de listín telefónico donde almacenaremos nombre, apellidos, dirección postal y todos los números de teléfono y e-mail que tengan nuestros contactos.

El esquema de base de datos será el siguiente:

agenda

Las relaciones serán 1 a muchos entre la tabla contactos y las tablas telefonos y e-mails.

Teniendo esto sabemos que necesitaremos 3 modelos que son contact, phone y email (ponemos los nombres en inglés para la pluralización de Rails), las tablas se llamarán igual.

En cuanto a controladores necesitaremos también uno por modelo para definir las acciones de CRUD que son new, create, index, show, edit, update, delete y destroy.

rails new contacts -d mysql

Y en mysql

mysql> CREATE DATABASER contacts_development;
mysql> GRANT ALL PRIVILEGES ON contacts_development.* TO 'edu'@'localhost' IDENTIFIED AS 'superpassword';

Y pondremos en el fichero /config/database.yml los datos:

username: edu 
password: superpassword

Y generamos los controllers

$ rails generate controller contacts new create index show edit update delete destroy
$ rails generate controller phones new create index show edit update delete destroy
$ rails generate controller emails new create index show edit update delete destroy

Ahora generamos los modelos y creamos las tablas

$ rails generate model contact
$ rails generate model phone
$ rails generate model email

Establecemos las relaciones entre tablas

/app/model/contact.rb

class Contact < ApplicationRecord 
    has_many :phones 
    has_many :emails 
end

/app/model/phone.rb

class Phone < ApplicationRecord
  belongs_to :contact 
end

/app/model/email.rb

class Email < ApplicationRecord
  belongs_to :contact
end

En nuestro caso no va a afectar mucho, pero al poner belongs_to y usar Rails 5, como es mi caso, está actuando el presence validator, que por defecto no viene activado en Rails 4, esto quiere decir que si no hay contacto no podemos crear ni teléfono ni email, algo que por otro lado tiene toda la lógica.

Pues llegados a este punto inicial ya podemos empezar a trabajar en nuestro CRUD y crear la página web con el formulario.

Para tener una apariencia «mona» vamos a poner la cabecera de bootstrap en nuestro layout por defecto y creamos las migraciones, es decir, la estructura de las tablas:

app/layouts/application.html.erb

<!DOCTYPE html>
<html>
  <head>
    <title>Contacts</title>
    <%= csrf_meta_tags %>

    <%= stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track': 'reload' %>
    <!-- Latest compiled and minified CSS -->
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
    <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
    <!-- Latest compiled and minified JavaScript -->
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
  </head>

  <body>
    <div class="container">
      <%= yield %>
    </div>
  </body>
</html>

db/migrate/20161113143332_create_contacts.rb

class CreateContacts < ActiveRecord::Migration[5.0]
  def change
    create_table :contacts do |t|
      t.string :nombre, :null => false
      t.string :apellidos, :null => false
      t.string :calle
      t.string :poblacion
      t.string :provincia
      t.string :pais
      t.timestamps
    end
  end
end

db/migrate/20161113143340_create_phones.rb

class CreatePhones < ActiveRecord::Migration[5.0]
  def change
    create_table :phones do |t|
      t.string :telefono
      t.string :contacts_id
      t.timestamps
    end
  end
end

db/migrate/20161113143345_create_emails.rb

class CreateEmails < ActiveRecord::Migration[5.0]
  def change
    create_table :emails do |t|
      t.string :email
      t.string :contacts_id
      t.timestamps
    end
  end
end

Ahora ya podemos migrar

$ rake db:migrate
== 20161113143332 CreateContacts: migrating ===================================
-- create_table(:contacts)
   -> 0.0359s
== 20161113143332 CreateContacts: migrated (0.0360s) ==========================

== 20161113143340 CreatePhones: migrating =====================================
-- create_table(:phones)
   -> 0.0368s
== 20161113143340 CreatePhones: migrated (0.0369s) ============================

== 20161113143345 CreateEmails: migrating =====================================
-- create_table(:emails)
   -> 0.0411s
== 20161113143345 CreateEmails: migrated (0.0412s) ============================

Sólo nos queda configurar una ruta por defecto en nuestro archivo de rutas y a hacer formularios

Rails.application.routes.draw do
  root 'contacts#index'
  match ':controller(/:action(/:id))', :via => [:get, :post]
  # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
end

Parece ser que en versiones posteriores de Rails se va a eliminar la ruta por defecto, pero mientras podremos seguirla utilizando.

Fijaros que la ruta por defecto la hemos creado sólo para dos verbos, get y post cuando lo suyo es que hubiéramos definido los 4 verbos get, post, patch, delete que se definen en REST.

También hemos definido que la página principal será el index de contacts.

Vamos a ir a construir el index del view contacts:

<h1>Lista de contactos</h1>
<p><%= link_to("Añadir contacto", {:controller => 'contacts', :action => 'new'}, data: { turbolinks: false } , :class => "btn btn-success" ) %></p>

<table class="table table-striped" summary="Contact List">
  <tr class="header">
    <th>
      Apellidos
    </th>
    <th>
      Nombre
    </th>
    <th>
      Acciones
    </th>
  </tr>
  <% @contact.each do |t| %>
  <tr>
    <td>
      <%= t.apellidos %>
    </td>
    <td>
      <%= t.nombre %>
    </td>
    <td>
      <%= link_to("Detalles", {:action => 'show', :id => t.id}, data: { turbolinks: false }, :class => "btn btn-primary") %>
      <%= link_to("Editar", {:action => 'edit', :id => t.id}, data: { turbolinks: false }, :class => "btn btn-primary") %>
      <%= link_to("Borrar", {:action => 'delete', :id => t.id}, data: { turbolinks: false }, :class => "btn btn-primary") %>
    </td>
  </tr>
  <% end %>
</table>

Los botones están así porque es la forma de definirlos en bootstrap y los turbolinks están en false porque si no falla en el momento en el que se entre desde otro enlace o se tire atrás.

Arriba hemos definido el enlace a new, así que aquí lo ponemos

<h1>Añadir Contacto</h1>
<table class="table table-striped" summary="Add Task" >
    <tr class="header">
        <th></th>
        <th>Contenido</th>
    </tr>
    <tr>
            <th>Apellidos</th>
            <%= form_for(:contact, :url => {:action => 'create', :id => @contact.id}) do |f| %>
            <td><%= f.text_field(:apellidos) %></td>
    </tr>
    <tr>
            <th>Nombre</th>
            <td><%= f.text_field(:nombre) %></td>
    </tr>

</table>
        <%= submit_tag("Añadir", :class => "btn btn-primary") %>
        <% end %>

Esto es realmente un formulario form_for en el que solicitamos los apellidos y el nombre, para que esto funcione en el controller tenemos que tener esta configuración

  def new
    @contact = Contact.new
  end

  def create
    @contact = Contact.new(contact_params)
    @contact.save
    redirect_to(:action => 'index')

  private
    def contact_params
      params.require(:contact).permit(:nombre, :apellidos, :id, :poblacion, :provincia, :pais)
    end

Hemos utilizado un método privado en create para que desde new sólo se pasen los parámetros definidos, ni más ni menos, al método contact_params no se puede acceder desde fuera. El formulario se añadiría en new, pero el se valida en create, donde no hay nada en la vista porque hemos creado una redirección al index.

Ahora vamos a hacer el editar que es igual al crear

app/views/contacts/edit.html.erb

<h1>Editar Contacto</h1>
<table class="table table-striped" summary="Add Task" >
    <tr class="header">
        <th></th>
        <th>Contenido</th>
    </tr>
    <tr>
            <th>Apellidos</th>
            <%= form_for(:contact, :url => {:action => 'update', :id => @contact.id}) do |f| %>
            <td><%= f.text_field(:apellidos) %></td>
    </tr>
    <tr>
            <th>Nombre</th>
            <td><%= f.text_field(:nombre) %></td>
    </tr>

</table>
        <%= submit_tag("Editar", :class => "btn btn-primary") %>
        <% end %>

Y en el controller

  def edit
    @contact = Contact.find(params[:id])
  end

  def update
    @contact = Contact.find(params[:id])
    @contact.update_attributes(contact_params)
    redirect_to(:action =>'index')
  end


  private
    def contact_params
      params.require(:contact).permit(:nombre, :apellidos, :id, :poblacion, :provincia, :pais)
    end

La única diferencia es que aquí para editar lo hacemos fijándonos en el parámetro :id

Partiendo de aquí el resto sería muy parecido, diferencias las tendríamos en borrar donde tendríamos que borrar los emails asociados y los teléfonos

  def delete
    @contact = Contact.find(params[:id])
  end


  def destroy
    @contact = Task.find(params[:id])
    @phone = Action.where(contact_id: @contact)
    @phone.each do |t|
      t.destroy
    @email = Action.where(contact_id: @contact)
    @email.each do |t|
      t.destroy
    end
    @contact.destroy
    redirect_to(:action => 'index')
  end

Con esto ya tendríamos una web de contactos, y a vosotros os dejo la creación de las páginas para los teléfonos y los emails.