window.pipedriveLeadboosterConfig = { base: 'leadbooster-chat.pipedrive.com', companyId: 11580370, playbookUuid: '22236db1-6d50-40c4-b48f-8b11262155be', versión: 2, } ;(function () { var w = window if (w.LeadBooster) { console.warn('LeadBooster ya existe') } else { w.LeadBooster = { q: [], on: function (n, h) { this.q.push({ t: 'o', n: n, h: h }) }, trigger: function (n) { this.q.push({ t: 't', n: n }) }, } } })() Incluir sub-recursos en una API REST - The Codest
The Codest
  • Quiénes somos
  • Servicios
    • Desarrollo de software
      • Desarrollo Frontend
      • Desarrollo backend
    • Staff Augmentation
      • Desarrolladores frontales
      • Desarrolladores de backend
      • Ingenieros de datos
      • Ingenieros de la nube
      • Ingenieros de control de calidad
      • Otros
    • Asesoramiento
      • Auditoría y consultoría
  • Industrias
    • Fintech y Banca
    • E-commerce
    • Adtech
    • Tecnología sanitaria
    • Fabricación
    • Logística
    • Automoción
    • IOT
  • Valor para
    • CEO
    • CTO
    • Gestor de entregas
  • Nuestro equipo
  • Case Studies
  • Saber cómo
    • Blog
    • Meetups
    • Seminarios en línea
    • Recursos
Carreras profesionales Póngase en contacto
  • Quiénes somos
  • Servicios
    • Desarrollo de software
      • Desarrollo Frontend
      • Desarrollo backend
    • Staff Augmentation
      • Desarrolladores frontales
      • Desarrolladores de backend
      • Ingenieros de datos
      • Ingenieros de la nube
      • Ingenieros de control de calidad
      • Otros
    • Asesoramiento
      • Auditoría y consultoría
  • Valor para
    • CEO
    • CTO
    • Gestor de entregas
  • Nuestro equipo
  • Case Studies
  • Saber cómo
    • Blog
    • Meetups
    • Seminarios en línea
    • Recursos
Carreras profesionales Póngase en contacto
Flecha atrás VOLVER
2022-03-22
Desarrollo de software

Incluir subrecursos en una API REST

The Codest

Krzysztof Buszewicz

Senior Software Engineer

Crearemos una aplicación de estantería para listar libros con (o sin) datos de autor.

¿Qué vamos a hacer?

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.

"Criterios de aceptación"

  • El usuario puede listar los libros.
  • El usuario puede pasar incluye para cargar los recursos asociados (autor).
  • incluye tiene un formato de cadena: palabras separadas por comas, que representan recursos anidados.
  • Deberíamos tener algunas constantes que definan qué recursos se pueden incluir para qué acción.

Herramientas

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.

La aplicación

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.

Artículos relacionados

Fintech

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...

The Codest
Pawel Muszynski Software Engineer
Desarrollo de software

Polimorfismo en Ruby y GraphQL

En este artículo, voy a presentar el uso del polimorfismo en GraphQL. Antes de empezar, sin embargo, vale la pena recordar lo que el polimorfismo y GraphQL son.

Lukasz Brzeszcz
E-commerce

Dilemas de ciberseguridad: Filtraciones de datos

La fiebre prenavideña está en pleno apogeo. En busca de regalos para sus seres queridos, la gente está cada vez más dispuesta a "asaltar" las tiendas en línea

The Codest
Jakub Jakubowicz CTO y Cofundador
Desarrollo de software

Una sencilla aplicación Ruby desde cero con Active Record

MVC es un patrón de diseño que divide las responsabilidades de una aplicación para que sea más fácil moverse por ella. Rails sigue este patrón de diseño por convención.

The Codest
Damian Watroba Software Engineer

Suscríbase a nuestra base de conocimientos y manténgase al día de la experiencia del sector informático.

    Quiénes somos

    The Codest - Empresa internacional de desarrollo de software con centros tecnológicos en Polonia.

    Reino Unido - Sede central

    • Oficina 303B, 182-184 High Street North E6 2JA
      Londres, Inglaterra

    Polonia - Centros tecnológicos locales

    • Parque de oficinas Fabryczna, Aleja
      Pokoju 18, 31-564 Cracovia
    • Embajada del Cerebro, Konstruktorska
      11, 02-673 Varsovia, Polonia

      The Codest

    • Inicio
    • Quiénes somos
    • Servicios
    • Case Studies
    • Saber cómo
    • Carreras profesionales
    • Diccionario

      Servicios

    • Asesoramiento
    • Desarrollo de software
    • Desarrollo backend
    • Desarrollo Frontend
    • Staff Augmentation
    • Desarrolladores de backend
    • Ingenieros de la nube
    • Ingenieros de datos
    • Otros
    • Ingenieros de control de calidad

      Recursos

    • Hechos y mitos sobre la cooperación con un socio externo de desarrollo de software
    • De EE.UU. a Europa: ¿Por qué las startups estadounidenses deciden trasladarse a Europa?
    • Comparación de los polos de desarrollo de Tech Offshore: Tech Offshore Europa (Polonia), ASEAN (Filipinas), Eurasia (Turquía)
    • ¿Cuáles son los principales retos de los CTO y los CIO?
    • The Codest
    • The Codest
    • The Codest
    • Privacy policy
    • Condiciones de uso del sitio web

    Copyright © 2025 por The Codest. Todos los derechos reservados.

    es_ESSpanish
    en_USEnglish de_DEGerman sv_SESwedish da_DKDanish nb_NONorwegian fiFinnish fr_FRFrench pl_PLPolish arArabic it_ITItalian jaJapanese ko_KRKorean nl_NLDutch etEstonian elGreek es_ESSpanish