5 ejemplos del mejor uso de Ruby
¿Te has preguntado alguna vez qué podemos hacer con Ruby? Bueno, el cielo es probablemente el límite, pero estaremos encantados de hablar de algunos casos más o menos conocidos...
Crearemos una aplicación de estantería para listar libros con (o sin) datos de autor.
Crearemos una aplicación de estantería para listar libros con (o sin) datos de autor. Habrá un único 1TP63Índice
y algunas semillas. Esta será una aplicación de ejemplo para mostrar cómo se puede dar a un usuario el control sobre incluido sub-recursos en una API REST-ish.
incluye
para cargar los recursos asociados (autor
).incluye
tiene un formato de cadena: palabras separadas por comas, que representan recursos anidados.Utilizaremos impresora azul
como serializador, porque es independiente del formato y bastante flexible. Esta es una única gema que añadiremos al conjunto de herramientas estándar de rails.
Vamos a crear una aplicación de ejemplo. No vamos a añadir el marco de pruebas, ya que está fuera de nuestro alcance.
rieles estantería nueva -T
Ahora crea Autor
modelo:
rails g modelo autor nombre:cadena
#=> invocar active_record
#=> crear db/migrate/20211224084524_create_authors.rb
#=> crear app/models/author.rb
Y Reserve
:
rails g modelo libro autor:referencias título:cadena
# => invocar registro_activo
# => crear db/migrate/20211224084614_create_books.rb
# => crear app/modelos/libro.rb
Necesitaremos semillas:
# db/seeds.rb
dumas = Author.create(nombre: 'Alexandre Dumas')
lewis = Author.create(nombre: 'C.S. Lewis')
martin = Author.create(nombre: 'Robert C. Martin')
Book.create(author: dumas, title: 'Los tres mosqueteros')
Book.create(author: lewis, title: 'El león, la bruja y el armario')
Book.create(author: martin, title: "Código limpio")
Y ahora estamos listos para ejecutar migraciones y sembrar la base de datos:
rails db:migrate && rails db:seed
Añadamos tiene_muchos
para libros en Autor
modelo:
# app/models/author.rb
class Autor < ApplicationRecord
has_many :libros
end
Es hora de escribir un controlador que devuelva nuestros datos. Usaremos API
así que primero vamos a añadir un acrónimo a las inflexiones:
# config/initializers/inflections.rb
ActiveSupport::Inflector.inflections(:es) do |inflect|
inflect.acronym 'API'
end
Ok, vamos a añadir nuestro serializador a Gemfile
:
# Añadir a Gemfile
gema 'blueprinter
Y por supuesto instalarlo:
instalación del paquete
Entonces podremos construir nuestros planos:
# app/blueprints/author_blueprint.rb
clase AuthorBlueprint < Blueprinter::Base
identificador :id
campos :name
fin
# app/blueprints/book_blueprint.rb
clase BookBlueprint < Blueprinter::Base
identificador :id
campos :título
asociación :autor, blueprint: AuthorBlueprint
end
Añadir un controlador base para API
:
# app/controllers/api/v1/base_controller.rb
módulo API
módulo V1
clase BaseController < ActionController::API
end
end
end
Y la versión preliminar de nuestro Controlador de libros
:
# app/controllers/api/v1/books_controller.rb
módulo API
módulo V1
class LibrosControlador < BaseControlador
def índice
libros = Book.all
renderizar json: BookBlueprint.render(books)
end
end
end
fin
Por supuesto, también debemos definir el enrutamiento:
# config/routes.rb
Rails.application.routes.draw do
namespace :api do
namespace :v1 do
recursos :libros, sólo: :índice
end
end
end
Probemos lo que hemos hecho hasta ahora:
carriles s
rizo http://localhost:3000/api/v1/books
# => [{"id":1, "author":{"id":1, "name": "Alexandre Dumas"}, "title": "Los Tres Mosqueteros"},{"id":2, "author":{"id":2, "name": "C.S. Lewis"}, "title": "El león, la bruja y el armario"},{"id":3, "author":{"id":3, "name": "Robert C. Martin"}, "title": "Clean Código"}]
Los datos parecen estar bien, ¿qué pasa con los registros?
Registros de solicitudes # (n+1)
Iniciado GET "/api/v1/books" para 127.0.0.1 en 2021-12-24 10:19:40 +0100
Procesado por API::V1::BooksController#index como */*
Carga de libros (0.1ms) SELECT "libros".* FROM "libros"
↳ app/controllers/api/v1/books_controller.rb:7:in `index'
Carga de autores (0.1ms) SELECT "authors".* FROM "authors" WHERE "authors". "id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]
↳ app/controllers/api/v1/books_controller.rb:7:in `index'
Carga de autores (0.1ms) SELECT "autores".* FROM "autores" WHERE "autores". "id" = ? LIMIT ? [["id", 2], ["LIMIT", 1]]
↳ app/controllers/api/v1/books_controller.rb:7:in `index'
Carga de autores (0.1ms) SELECT "autores".* FROM "autores" WHERE "autores". "id" = ? LIMIT ? [["id", 3], ["LIMIT", 1]]
↳ app/controllers/api/v1/books_controller.rb:7:in `index'
Completado 200 OK en 6ms (Vistas: 0.1ms | ActiveRecord: 0.4ms | Asignaciones: 3134)
Al utilizar la asociación en nuestros serializadores introducimos n+1
problema. Queremos eliminarlo añadiendo al usuario un control sobre lo que solicita en este endpoint. Así que debería ser capaz de cargar sólo los libros, o pasar el parámetro includes y obtener también los autores, pero preferiblemente sin el parámetro n+1
.
Vamos a definir una constante que mantendrá una información sobre qué asocs de libros puede incluir el usuario en libros#index
acción:
# lib/constants/books/includes.rb
módulo Constants
módulo Books
módulo Includes
PERMITIDO = {
índice: %i[
autor
].freeze
}.congelar
fin
end
end
A continuación, definimos un espacio de nombres para las constantes de objetos vacíos:
# lib/constants/empty.rb
módulo Constants
módulo Empty
HASH = {}.congelar
end
end
Y aquí está nuestro servicio principal para permitir incluye. Creo que el código es bastante explica por sí mismo, algunas piezas de magia
sólo se asignan en #default_resources_key
y #propósito_por_defecto
. Estos métodos están definidos para permitirnos llamar a permit includes pasando sólo params en los controladores de rails. La salida será el hash que almacena verdadero
para cada inclusión permitida.
# app/services/permit_includes.rb
require 'constantes/vacío'
require 'constantes/libros/incluidos'
clase PermitIncludes
Vacío = Constantes::Vacío
COMMA = ','
SLASH = '/'
INCLUDES_FORMAT = /A[a-z]+(,[a-z]+)*z/.freeze
ALLOWED_INCLUDES = {
libros: Constantes::Libros::Includes::PERMITIDOS
}.congelar
def call(params, resources: default_resources_key(params), purpose: default_purpose(params))
return Empty::HASH unless includes_sent?(parámetros)
return Empty::HASH unless includes_valid?(parámetros)
requested_includes = parse_includes(parámetros)
allowed_includes = filter_includes(requested_includes, resources, purpose)
allowed_includes.index_with(true)
fin
privado
def default_resources_key(params)
raise(ArgumentError, 'params :controller key must be a string') unless params[:controller].is_a?(String)
params[:controlador].split(SLASH).last&.to_sym
end
def default_purpose(params)
raise(ArgumentError, 'params :action key must be a string') unless params[:action].is_a?(String)
params[:action].to_sym
end
def ¿incluye_envío?(parámetros)
params.key?(:includes)
end
def ¿incluye_valido?(parámetros)
return false unless params[:includes].is_a?(String)
params[:includes].match?(INCLUDES_FORMAT)
end
def parse_includes(params)
params[:includes].split(COMMA).map(&:to_sym)
end
def filter_includes(requested_includes, resources_key, purpose)
requested_includes & ALLOWED_INCLUDES[resources_key][purpose]
end
end
Ahora tenemos que utilizar las claves para cargar los includes y pasar el propio hash de los inlcudes al serializador:
# app/controllers/api/v1/books_controller.rb
módulo API
módulo V1
class LibrosControlador < BaseControlador
def índice
includes = PermitIncludes.new.call(params)
libros = Book.includes(includes.keys).all
renderizar json: BookBlueprint.render(books, includes: includes)
end
end
end
fin
Y así es como debemos ajustar nuestro serializador: cargamos la asociación sólo si está incluida:
# app/blueprints/book_blueprint.rb
clase BookBlueprint (_nombre_campo, _libro, opciones) {
options[:includes] && options[:includes][:author]
}
end
Probémoslo de nuevo:
carriles s
rizo http://localhost:3000/api/v1/books
# => [{"id":1, "title": "Los tres mosqueteros"},{"id":2, "title": "El león, la bruja y el armario"},{"id":3, "title": "Código limpio"}]
Registros de peticiones # (sólo cargamos libros)
Iniciado GET "/api/v1/books" para ::1 en 2021-12-24 10:33:41 +0100
Procesado por API::V1::BooksController#index como */*
(0.1ms) SELECT sqlite_version(*)
↳ app/controllers/api/v1/books_controller.rb:8:in `index'
Carga de libros (0.1ms) SELECT "books".* FROM "books"
↳ app/controllers/api/v1/books_controller.rb:8:in `index'
Completado 200 OK en 9ms (Vistas: 0.1ms | ActiveRecord: 0.9ms | Asignaciones: 4548)
Bien, no hemos pasado los includes así que sólo tenemos libros, sin autores. Ahora vamos a solicitarlos:
curl 'http://localhost:3000/api/v1/books?includes=author'
# => [{"id":1, "author":{"id":1, "name": "Alexandre Dumas"}, "title": "Los Tres Mosqueteros"},{"id":2, "author":{"id":2, "name": "C.S. Lewis"}, "title": "El león, la bruja y el armario"},{"id":3, "author":{"id":3, "name": "Robert C. Martin"}, "title": "Código limpio"}]%
Registros de solicitudes # (eliminado n+1)
Iniciado GET "/api/v1/books?includes=author" para ::1 el 2021-12-24 10:38:23 +0100
Procesado por API::V1::BooksController#index como */*
Parámetros: {"includes"=>"author"}
Carga de libros (0.1ms) SELECT "books".* FROM "books"
↳ app/controllers/api/v1/books_controller.rb:8:in `index'
Author Load (0.2ms) SELECT "authors".* FROM "authors" WHERE "authors". "id" IN (?, ?, ?) [["id", 1], ["id", 2], ["id", 3]]
↳ app/controllers/api/v1/books_controller.rb:8:in `index'
Completado 200 OK en 17ms (Vistas: 0.1ms | ActiveRecord: 0.7ms | Asignaciones: 7373)
¡Genial! Tenemos la asociación cargado y eliminado n+1
problema. El servicio se puede utilizar para cualquier recurso, todo lo que queremos hacer es añadir inlcudes constantes permitidas en el formato adecuado y añadirlos a PermitIncludes::ALLOWED_INCLUDES
.
Hay que recordar que esto debe usarse probablemente con paginación (y precaución) porque incluir asociaciones puede "comerse" mucha memoria.