rails ruby

Ruby on Rails - introducción rápida

Introducción a Ruby on Rails presentando CRUD, relaciones de base de datos, correo y comunicación por sockets web.

Daniel Gustaw

Daniel Gustaw

13 min read

Ruby on Rails - introducción rápida

En 2019, reescribí un cierto sistema médico de Rails a PHP, y en 2021 de Rails a NodeJS. Quizás también te estés encontrando con sistemas basados en Rails que están perdiendo mantenimiento. Esta introducción te ayudará a familiarizarte rápidamente con los conceptos básicos de este framework.

Escribiremos un blog completamente desde cero. Me gustaría señalar que no estoy muy familiarizado ni con Ruby ni con Rails, así que en lugar de una extensa introducción, tenemos una recreación de mi proceso de aprendizaje.

Supuestos:

Configurando la aplicación - CRUD

Comenzaremos con la instalación de la versión adecuada de ruby.

curl -sSL https://get.rvm.io | bash -s stable --rails

rvm es una herramienta análoga a nvm - te permite gestionar la versión del intérprete, lo cual es excepcionalmente útil al trabajar con sistemas que utilizan diferentes versiones de intérpretes. Puedes leer sobre ello aquí:

RVM: Administrador de Versiones de Ruby - Instalando RVM

Creamos la aplicación con el siguiente comando:

rails new weblog && cd weblog

Este comando tarda mucho tiempo porque requiere la instalación de todos los paquetes gem y la compilación de node-sass.

El siguiente paso es generar automáticamente el código para realizar operaciones CRUD en una publicación. Las publicaciones tendrán un título y contenido.

rails generate scaffold post title:string body:text

Este comando genera una gran cantidad de archivos:

Uno de ellos es la migración de la base de datos, que está escrita en db/migrate/20210418121400_create_posts.rb y se ve así:

class CreatePosts < ActiveRecord::Migration[6.1]
  def change
    create_table :posts do |t|
      t.string :title
      t.text :body

      t.timestamps
    end
  end
end

Para sincronizar la base de datos con el resultado de esta migración, ingresamos

rails db:migrate

Aquí puede hacer la pregunta: “¿Qué base de datos?”. En el archivo config/database.yml podemos ver la configuración que indica que por defecto es sqlite. En el archivo db/schema.rb está el esquema de la base de datos.


Este es un buen lugar para una digresión. Al migrar sistemas basados en Ruby on Rails, me pregunté por qué el entorno de producción usa “sqlite”; pensé que alguien lo configuró deliberadamente de esta manera. Resulta que era suficiente no cambiar la configuración en este archivo. Otro tema que ocupó mi mente hace dos años fue el campo “updated_at” en tablas que no manejaban edición. Al ver “updated_at” y carecer de documentación, pensé que había un proceso para editar estas tablas; sin embargo, esto también es una consecuencia de la configuración predeterminada de “rails”, que en todas partes agrega estas columnas.


Para iniciar el servidor, utilizamos el comando

rails server

Una gran ventaja de Rails es que ya podemos usar un CRUD funcional en el enlace

http://127.0.0.1:3000/posts

Después de crear manualmente una publicación, obtenemos:

Lo que es aún más agradable es que también tenemos una “api” disponible en /posts.json

Desafortunadamente, el intento de crear una publicación a través de la API.

http POST localhost:3000/posts.json title="Hej" body="Ok"

termina con un error

Can't verify CSRF token authenticity.

Para deshabilitar la protección “CSRF” en el archivo app/controllers/application_controller.rb, configura la opción protect_from_forgery.

class ApplicationController < ActionController::Base
  protect_from_forgery with: :null_session
end

Ahora el guardado de la publicación a través de la API funciona. Ambos

http POST localhost:3000/posts.json title=ok

cómo y

http POST localhost:3000/posts.json body=ok

publicarán sus entradas sin validar su corrección.

Para hacer cumplir la presencia del parámetro title en el post, en el archivo app/models/post.rb añadimos la bandera validates_presence_of.

class Post < ApplicationRecord
  validates_presence_of :title
end

Gracias a ello, será imposible añadir publicaciones sin un título tanto en la página

cómo y a través de API

Depuración - Consola de Rails

Una herramienta muy útil al trabajar con Ruby on Rails es la consola que se puede acceder ingresando el comando:

rails console

Permite el acceso interactivo a los datos utilizando el lenguaje Ruby y objetos definidos en Rails. Por ejemplo, veremos la primera publicación ingresando

Post.first

Para obtener todas las publicaciones que escribimos

Post.all

Las publicaciones creadas desde ayer hasta mañana se recibirán por escrito.

Post.where(created_at: Date.yesterday..Date.tomorrow)

Se puede transformar fácilmente en una consulta SQL agregando la propiedad to_sql al final.

Post.where(created_at: Date.yesterday..Date.tomorrow).to_sql

Para crear una nueva publicación escribimos

Post.create! title: 'Hello', body: 'World'

Relaciones Entre Tablas

Un ejemplo típico de una relación con respecto a las publicaciones son los comentarios. No necesitamos los mismos controladores y vistas para ellos que para las publicaciones, por lo que en lugar de scaffold, usaremos la bandera resource para la generación.

rails generate resource comment post:references body:text

Podemos ver la lista completa de generadores disponibles ingresando el comando:

rails generate

o leyendo la documentación

La línea de comandos de Rails — Guías de Ruby on Rails

Mientras tanto, volveremos a los archivos generados por la opción resource.

Se ha creado una migración aquí nuevamente, esta vez conteniendo:

class CreateComments < ActiveRecord::Migration[6.1]
  def change
    create_table :comments do |t|
      t.references :post, null: false, foreign_key: true
      t.text :body

      t.timestamps
    end
  end
end

Para ejecutarlo, ingresamos

rails db:migrate

Ahora hablemos sobre el enrutamiento. No tiene sentido pedir todos los comentarios. Siempre están relacionados con la publicación a la que pertenecen. Así que en el archivo config/routes.yml, reemplazamos las ocurrencias adyacentes.

Rails.application.routes.draw do
  resources :posts
  resources :comments
end

a configuración que permite los comentarios anidados en la publicación

Rails.application.routes.draw do
  resources :posts do
    resources :comments
  end
end

La visualización del enrutamiento es posible gracias al comando:

rails routes

En cuanto a la dirección de la relación, en este momento los comentarios pertenecen a las publicaciones como se describe en el archivo app/models/comment.rb

class Comment < ApplicationRecord
  belongs_to :post
end

Pero las publicaciones no tienen una relación designada con los comentarios, lo que solucionaremos agregando has_many a app/models/post.rb

class Post < ApplicationRecord
  has_many :comments
  validates_presence_of :title
end

En la consola ahora podemos crear un comentario de muestra.

Post.second.comments.create! body: "My first comment to second post"

Para mostrar comentarios y añadirlos, escribiremos fragmentos de vista auxiliares (parciales). app/views/comments/_comment.html.erb se utilizará para mostrar un solo comentario.

<p><%= comment.body %> -- <%= comment.created_at.to_s(:long) %></p>

Por otro lado, app/views/comments/_new.html.erb será el formulario para crear un comentario.

<%= form_for([ @post, Comment.new], remote: true) do |form| %>
  Your comment: <br/>
  <%= form.text_area :body, size: '50x2' %><br/>
  <%= form.submit %>
<% end %>

Los adjuntaremos en la vista de publicación única agregando el código al archivo app/views/posts/show.html.erb

<hr>

<h2>Comments (<span id="count"><%= @post.comments.count %></span>)</h2>

<div id="comments">
   <%= render @post.comments %>
</div>

<%= render 'comments/new', post: @post %>

Ahora nuestra vista del post se verá de la siguiente manera.

Aunque parece listo para usar, la función de añadir comentarios aún no está disponible. Solo preparamos la vista, pero falta la lógica para guardar comentarios en la base de datos y vincularlos a las publicaciones.

Para integrarlo, necesitamos manejar la creación de comentarios en el controlador app/controllers/comments_controller.rb

class CommentsController < ApplicationController
  before_action :set_post

  def create
    @post.comments.create! comments_params
    redirect_to @post
  end

  private

  def set_post
    @post = Post.find(params[:post_id])
  end

  def comments_params
    params.required(:comment).permit(:body)
  end

end

Echemos un vistazo más de cerca. Comienza con la opción before_action, que establece la publicación según el parámetro de la url. Luego, en create, usamos esta publicación para crear un comentario, cuyos parámetros provienen de comments_params, que los recupera del cuerpo de la solicitud.

A continuación, hay una redirección a la página de publicaciones. Funciona muy bien en la página.

Pero si queremos crear publicaciones desde el nivel de API, cada vez que se nos redirige a la publicación, la veremos sin comentarios. Si reemplazamos

redirect_to @post

en el controlador usando instrucciones análogas a las del post

    respond_to do |format|
      if @post.save
        format.html { redirect_to @post, notice: "Comment was successfully created." }
        format.json { render :show, status: :created, location: @post }
      else
        format.html { render :new, status: :unprocessable_entity }
        format.json { render json: @post.errors, status: :unprocessable_entity }
      end
    end

tendremos un error

Es así porque ahora los comentarios requieren estructuración al organizarlos en un archivo JSON. Esto se resuelve gracias a la fantástica biblioteca jbuilder.

rails/jbuilder

Al crear el archivo app/views/comments/show.json.jbuilder con el contenido

json.partial! "posts/post", post: @post
json.comments @post.comments, :id, :body, :created_at

configuraremos el servidor para que responda con la vista de la publicación que contiene una lista de todos los comentarios correspondientes una vez que se crea un comentario. Esta es una vista que corresponde a lo que vemos en la versión HTML, aunque no se ajusta a los principios REST.

Si quisiéramos mostrar este comentario específico, podemos usar la sintaxis

  def create
    comment = @post.comments.create! comments_params

    respond_to do |format|
      if @post.save
        format.html { redirect_to @post, notice: "Comment was successfully created." }
        format.json { render json: comment.to_json(include: [:post]) }
      else
        format.html { render :new, status: :unprocessable_entity }
        format.json { render json: @post.errors, status: :unprocessable_entity }
      end
    end

  end

en el controlador. Luego, en la vista de respuesta, veremos el comentario junto con la publicación.

Más información sobre el formato se puede leer aquí:

Renderizando JSON en una API de Rails

Enviando correos electrónicos

Una función muy común en los servicios web es enviar correos electrónicos en respuesta a ciertos eventos. En lugar de reescribir el código, utilizaremos un generador:

rails generate mailer comments submitted

Este es un correo electrónico enviando un saludo. Lo primero que haremos es configurar los datos que se inyectarán en las plantillas. En el archivo comments_mailer.rb, escribimos el código:

class CommentsMailer < ApplicationMailer
  def submitted(comment)
    @comment = comment

    mail to: "[email protected]", subject: 'New comment'
  end
end

En app/views/comments_mailer tenemos dos archivos de plantilla. Para la vista HTML, es el archivo submitted.html.erb. Lo modificaremos para que, utilizando el parcial definido anteriormente, muestre el nuevo comentario:

<h1>New comment on post: <%= @comment.post.title %></h1>

<%= render @comment %>

En el archivo submitted.text.erb, ya no podemos usar render, por lo que simplificaremos la vista de texto al formulario:

New comment on post: <%= @comment.post.title %>: <%= @comment.body %>

Lo asombroso de Rails es que tenemos una vista lista para previsualizar estos correos electrónicos sin tener que enviarlos. Para usarla, solo necesitamos especificar el comentario que mostraremos. Para este propósito, en el archivo test/mailers/previews/comments_mailer_preview.rb, la línea

CommentsMailer.submitted

cambiamos a

CommentsMailer.submitted Comment.first

En la dirección

http://localhost:3000/rails/mailers/comments_mailer/submitted

Podemos ver una vista previa de este correo electrónico

Sin embargo, no podemos esperar que este correo electrónico se envíe de inmediato. Para incluir su envío, necesitamos agregar una línea.

CommentsMailer.submitted(comment).deliver_later

en el controlador de comentarios. El controlador completo debería verse así:

class CommentsController < ApplicationController
  before_action :set_post

  def create
    comment = @post.comments.create! comments_params
    CommentsMailer.submitted(comment).deliver_later

    respond_to do |format|
      if @post.save
        format.html { redirect_to @post, notice: "Comment was successfully created." }
        format.json { render json: comment.to_json(include: [:post]) }
      else
        format.html { render :new, status: :unprocessable_entity }
        format.json { render json: @post.errors, status: :unprocessable_entity }
      end
    end

  end

  private

  def set_post
    @post = Post.find(params[:post_id])
  end

  def comments_params
    params.required(:comment).permit(:body)
  end

end

El flag “deliver_later” te permite adjuntar el envío de un correo electrónico al ciclo interno de Ruby on Rails, que lo enviará lo antes posible sin bloquear la ejecución del resto del código. Crear un comentario aún no enviará el correo electrónico al correo real, pero en la consola, veremos que tal acción se habría tomado si el envío estuviera completamente configurado.

No iremos por ese camino, pero si deseas completar la configuración, lee sobre smtp_settings y delivery_method en la documentación:

Conceptos básicos de Action Mailer — Guías de Ruby on Rails

Ahora pasaremos a la comunicación en tiempo real.

Cable - comunicación a través de web socket

Para usar la comunicación en tiempo real, necesitamos un canal. Lo generaremos con el comando:

rails generate channel comments

En el archivo app/channels/comments_channel.rb que contiene:

class CommentsChannel < ApplicationCable::Channel
  def subscribed
    # stream_from "some_channel"
  end

  def unsubscribed
    # Any cleanup needed when channel is unsubscribed
  end
end

agregando el método broadcast

  def self.broadcast(comment)
    broadcast_to comment.post, comment:
      CommentsController.render(partial: 'comments/comment', locals: { comment: comment })
  end

también haremos una simplificación de que la suscripción solo se aplicará al último post. Nuestro objetivo es mostrar los conceptos básicos de Rails, así que nos enfocaremos en llevar el mecanismo del canal a la presentación, saltando este aspecto. Como parte de esta simplificación, escribimos

  def subscribed
    stream_for Post.last
  end

Para habilitar el envío de mensajes al navegador, agregamos la línea

CommentsChannel.broadcast(comment)

con el correo electrónico incluido en el controlador de comentarios.

Un archivo con la configuración del canal app/javascript/channels/comments_channel.js se adjuntará al navegador. Lo configuramos de manera que, en respuesta a un comentario que se adjunta a la publicación (canal), se debe agregar al final del hilo, y el contador de comentarios debe aumentar en 1:

    received(data) {
        const commentsElement = document.querySelector('#comments');
        const countElement = document.querySelector('#count');

        if (commentsElement) {
            commentsElement.innerHTML += data.comment
        }
        if (countElement) {
            countElement.innerHTML = String(1 + parseInt(countElement.innerHTML))
        }
    }

El efecto es el siguiente:

Para un estudio más profundo, recomiendo los siguientes materiales:

Tutorial de Ruby on Rails - Tutorialspoint

Ruby on Rails

Other articles

You can find interesting also.