The Codest
  • Sobre nós
  • Serviços
    • Desenvolvimento de software
      • Desenvolvimento de front-end
      • Desenvolvimento backend
    • Staff Augmentation
      • Programadores Frontend
      • Programadores de back-end
      • Engenheiros de dados
      • Engenheiros de nuvem
      • Engenheiros de GQ
      • Outros
    • Aconselhamento
      • Auditoria e consultoria
  • Indústrias
    • Fintech e Banca
    • E-commerce
    • Adtech
    • Tecnologia da saúde
    • Fabrico
    • Logística
    • Automóvel
    • IOT
  • Valor para
    • CEO
    • CTO
    • Gestor de entregas
  • A nossa equipa
  • Case Studies
  • Saber como
    • Blogue
    • Encontros
    • Webinars
    • Recursos
Carreiras Entrar em contacto
  • Sobre nós
  • Serviços
    • Desenvolvimento de software
      • Desenvolvimento de front-end
      • Desenvolvimento backend
    • Staff Augmentation
      • Programadores Frontend
      • Programadores de back-end
      • Engenheiros de dados
      • Engenheiros de nuvem
      • Engenheiros de GQ
      • Outros
    • Aconselhamento
      • Auditoria e consultoria
  • Valor para
    • CEO
    • CTO
    • Gestor de entregas
  • A nossa equipa
  • Case Studies
  • Saber como
    • Blogue
    • Encontros
    • Webinars
    • Recursos
Carreiras Entrar em contacto
Seta para trás VOLTAR
2022-03-22
Desenvolvimento de software

Incluindo sub-recursos em uma API REST-ish

The Codest

Krzysztof Buszewicz

Sénior Software Engineer

Vamos criar uma aplicação de estante para listar livros com (ou sem) dados de autores.

O que é que vamos fazer?

Vamos criar uma aplicação de estante para listar livros com (ou sem) dados de autores. Haverá um único #index e algumas sementes. Esta será uma aplicação de exemplo para mostrar como se pode dar a um utilizador controlo sobre sub-recursos numa API REST-ish.

"Critérios de aceitação"

  • O utilizador pode listar os livros.
  • O utilizador pode passar inclui parâmetro de consulta para carregar os recursos associados (autor).
  • inclui O parâmetro de consulta tem um formato de cadeia de caracteres: palavras separadas por vírgulas, que representam recursos aninhados.
  • Deveríamos ter algumas constantes que definissem quais os recursos que podem ser incluídos para cada ação.

Ferramentas

Utilizaremos impressora azul como serializador, porque ele é independente de formato e bastante flexível. Esta é a única gem que adicionaremos ao conjunto de ferramentas padrão do rails.

A aplicação

Vamos criar uma aplicação de exemplo. Não vamos adicionar uma estrutura de teste porque está fora do nosso âmbito.

carris nova estante -T

Agora crie Autor modelo:

rails g model author name:string
#=> invoke active_record
#=> create db/migrate/20211224084524_create_authors.rb
#=> create app/models/author.rb

E Livro:

rails g model book author:references title:string
# => invocar active_record
# => create db/migrate/20211224084614_create_books.rb
# => create app/models/book.rb

Vamos precisar de algumas sementes:

# db/seeds.rb

dumas = Author.create(name: 'Alexandre Dumas')
lewis = Author.create(name: 'C.S. Lewis')
martin = Author.create(name: 'Robert C. Martin')

Book.create(author: dumas, title: 'The Three Musketeers')
Book.create(autor: lewis, título: 'O Leão, a Feiticeira e o Guarda-Roupa')
Livro.criar(autor: martin, título: 'Código Limpo')

E agora estamos prontos para executar migrações e semear o banco de dados:

rails db:migrate && rails db:seed

Vamos acrescentar tem_muitos para livros em Autor modelo:

# app/models/author.rb

class Author < ApplicationRecord
  has_many :books
fim

É altura de escrever um controlador que devolverá os nossos dados. Vamos usar o API por isso, primeiro vamos adicionar um acrónimo às inflexões:

# config/initializers/inflections.rb

ActiveSupport::Inflector.inflections(:en) do |inflect|
  inflect.acronym 'API'
end

Ok, vamos adicionar nosso serializador ao Ficheiro Gemfile:

# Adicionar ao Gemfile

gem 'blueprinter'

E, claro, instalá-lo:

instalação do pacote

Depois podemos construir os nossos projectos:

# app/blueprints/author_blueprint.rb

classe AuthorBlueprint < Blueprinter::Base
  identificador :id

  campos :nome
fim
# app/blueprints/book_blueprint.rb

classe BookBlueprint < Blueprinter::Base
  identificador :id

  campos :title

  associação :author, blueprint: AuthorBlueprint
fim

Adicionar um controlador de base para API:

# app/controllers/api/v1/base_controller.rb

módulo API
  módulo V1
    classe BaseController < ActionController::API
    fim
  fim
end

E a versão preliminar do nosso LivrosControlador:

# app/controllers/api/v1/books_controller.rb

módulo API
  módulo V1
    classe LivrosController < BaseController
      def index
        livros = Book.all

        renderizar json: BookBlueprint.render(books)
      fim
    fim
  fim
fim

É claro que também temos de definir o encaminhamento:

# config/routes.rb

Rails.application.routes.draw do
  namespace :api do
    namespace :v1 do
      recursos :livros, apenas: :índice
    fim
  fim
fim

Vamos testar o que fizemos até agora:

carris s 
curl http://localhost:3000/api/v1/books

# => [{"id":1, "author":{"id":1, "name": "Alexandre Dumas"}, "title": "The Three Musketeers"},{"id":2, "author":{"id":2, "name": "C.S. Lewis"}, "title": "O Leão, a Feiticeira e o Guarda-Roupa"},{"id":3, "author":{"id":3, "name": "Robert C. Martin"}, "title": "Clean Código"}]

Os dados parecem estar bem, mas e os registos?

Registos de pedidos # (n+1)

Iniciado GET "/api/v1/books" para 127.0.0.1 em 2021-12-24 10:19:40 +0100
Processamento por API::V1::BooksController#index como */*
  Carregamento de livros (0.1ms) SELECT "livros".* FROM "livros"
  app/controllers/api/v1/books_controller.rb:7:in `index'
  Autor Carga (0.1ms) SELECT "autores".* FROM "autores" WHERE "autores". "id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
  ↳ app/controllers/api/v1/books_controller.rb:7:in `index'
  Carga do autor (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 do autor (0.1ms) SELECT "autores".* FROM "autores" WHERE "autores". "id" = ? LIMIT ?  [["id", 3], ["LIMIT", 1]]
  ↳ app/controllers/api/v1/books_controller.rb:7:in `index'
Concluído 200 OK em 6ms (Visualizações: 0.1ms | ActiveRecord: 0.4ms | Atribuições: 3134)

Ao utilizar a associação nos nossos serializadores, introduzimos n+1 problema. Queremos eliminá-lo, adicionando ao utilizador um controlo sobre o que pede neste ponto final. Assim, ele deve ser capaz de carregar apenas livros, ou passar o parâmetro includes e obter autores também, mas de preferência sem o parâmetro n+1.

Vamos definir uma constante que irá manter uma informação sobre quais os assocs de livros que o utilizador pode incluir em livros#index ação:

# lib/constants/books/includes.rb

módulo Constantes
  módulo Livros
    módulo Includes
      ALLOWED = {
        índice: %i[
          autor
        ].freeze
      }.freeze
    fim
  fim
fim

Em seguida, definimos um espaço de nomes para constantes de objectos vazios:

# lib/constants/empty.rb

módulo Constantes
  módulo Vazio
    HASH = {}.freeze
  fim
fim

E aqui está o nosso serviço principal para permitir includes. Penso que o código é bastante auto-explicativo, algumas partes do mágico só são atribuídos em #default_resources_key e #default_purpose. Estes métodos são definidos para permitir nós para chamar o permit inclui passar apenas params nos controladores do rails. A saída será o hash que armazena verdadeiro para cada inclusão permitida.

# app/services/permit_includes.rb

require 'constants/empty'
require 'constants/books/includes'

classe PermitIncludes
  Vazio = Constantes::Vazio

  COMMA = ','
  SLASH = '/'

  INCLUDES_FORMAT = /A[a-z]+(,[a-z]+)*z/.freeze
  ALLOWED_INCLUDES = {
    livros: Constants::Books::Includes::ALLOWED
  }.freeze

  def call(params, resources: default_resources_key(params), purpose: default_purpose(params))
    return Empty::HASH unless includes_sent?(params)
    return Empty::HASH unless includes_valid?(params)

    inclui_pedido = inclui_parado(params)
    allowed_includes = filter_includes(requested_includes, resources, purpose)

    allowed_includes.index_with(true)
  fim

  privado

  def default_resources_key(params)
    raise(ArgumentError, 'params :controller key must be a string') unless params[:controller].is_a?(String)

    params[:controller].split(SLASH).last&.to_sym
  fim

  def default_purpose(params)
    raise(ArgumentError, 'params :action key must be a string') unless params[:action].is_a?(String)

    params[:action].to_sym
  fim

  def includes_sent?(params)
    params.key?(:includes)
  end

  def includes_valid?(params)
    return false unless params[:includes].is_a?(String)

    params[:includes].match?(INCLUDES_FORMAT)
  fim

  def parse_includes(params)
    params[:includes].split(COMMA).map(&:to_sym)
  end

  def filter_includes(requested_includes, resources_key, purpose)
    reincludes_pedidos & ALLOWED_INCLUDES[resources_key][purpose]
  end end
fim

Agora precisamos de utilizar as chaves para carregar os includes e passar o próprio hash dos inlcudes para o serializador:

# app/controllers/api/v1/books_controller.rb

módulo API
  módulo V1
    classe LivrosController < BaseController
      def index
        includes = PermitIncludes.new.call(params)
        livros = Book.includes(includes.keys).all

        renderizar json: BookBlueprint.render(books, includes: includes)
      fim
    fim
  fim
fim

E é assim que devemos ajustar o nosso serializador - carregamos a associação apenas se estiver incluída:

# app/blueprints/book_blueprint.rb
classe BookBlueprint (_nome_do_campo, _livro, opções) {
                         options[:includes] && options[:includes][:author]
                       }
end

Vamos testá-lo novamente:

carris s
enrolar http://localhost:3000/api/v1/books
# => [{"id":1, "title": "Os Três Mosqueteiros"},{"id":2, "title": "O Leão, a Feiticeira e o Guarda-Roupa"},{"id":3, "title": "Código Limpo"}]
Registos de pedidos # (só carregamos livros)
Iniciado GET "/api/v1/books" para ::1 em 2021-12-24 10:33:41 +0100
Processamento por API::V1::BooksController#index como */*
   (0.1ms) SELECT sqlite_version(*)
  app/controllers/api/v1/books_controller.rb:8:in `index'
  Carregamento de livros (0.1ms) SELECT "livros".* FROM "livros"
  app/controllers/api/v1/books_controller.rb:8:in `index'
Concluído 200 OK em 9ms (Visualizações: 0.1ms | ActiveRecord: 0.9ms | Alocações: 4548)

Ótimo, ainda não passámos os includes, por isso só temos livros, sem autores. Vamos agora solicitá-los:

curl 'http://localhost:3000/api/v1/books?includes=author'
# => [{"id":1, "author":{"id":1, "name": "Alexandre Dumas"}, "title": "The Three Musketeers"},{"id":2, "author":{"id":2, "name": "C.S. Lewis"}, "título": "O Leão, a Feiticeira e o Guarda-Roupa"},{"id":3, "autor":{"id":3, "nome": "Robert C. Martin"}, "título": "Código Limpo"}]% 
Registos de pedidos # (eliminado n+1)

Iniciado GET "/api/v1/books?includes=author" para ::1 em 2021-12-24 10:38:23 +0100
Processamento por API::V1::BooksController#index como */*
  Parâmetros: {"includes"=>"author"}
  Carregamento de livros (0.1ms) SELECT "livros".* FROM "livros"
  app/controllers/api/v1/books_controller.rb:8:in `index'
  Autor Carga (0.2ms) SELECT "autores".* FROM "autores" WHERE "autores". "id" IN (?, ?, ?) [["id", 1], ["id", 2], ["id", 3]]
  ↳ app/controllers/api/v1/books_controller.rb:8:in `index'
Concluído 200 OK em 17ms (Visualizações: 0.1ms | ActiveRecord: 0.7ms | Alocações: 7373)

Fixe! Temos a associação carregada e eliminada n+1 problema. O serviço pode ser utilizado para qualquer recurso, tudo o que queremos fazer é adicionar constantes de inclusão permitidas no formato correto e adicioná-las a PermitIncludes::ALLOWED_INCLUDES.

É preciso lembrar que isto deve ser usado provavelmente com paginação (e com cuidado) porque a inclusão de associações pode "comer" muita memória.

Artigos relacionados

Fintech

5 exemplos da melhor utilização do Ruby

Já alguma vez pensou no que podemos fazer com Ruby? Bem, o céu é provavelmente o limite, mas temos todo o gosto em falar sobre alguns casos mais ou menos conhecidos...

The Codest
Pawel Muszynski Software Engineer
Desenvolvimento de software

Polimorfismo em Ruby e GraphQL

Neste artigo, apresentarei o uso de polimorfismo no GraphQL. Antes de começar, porém, vale a pena relembrar o que é polimorfismo e o que é GraphQL.

Lukasz Brzeszcz
E-commerce

Dilemas da cibersegurança: Fugas de dados

A corrida pré-natalícia está ao rubro. Em busca de presentes para os seus entes queridos, as pessoas estão cada vez mais dispostas a "invadir" as lojas em linha

The Codest
Jakub Jakubowicz CTO e cofundador
Desenvolvimento de software

Uma aplicação Ruby simples a partir do zero com Active Record

O MVC é um padrão de design que divide as responsabilidades de uma aplicação para facilitar a sua movimentação. O Rails segue esse padrão de design por convenção.

The Codest
Damian Watroba Software Engineer

Subscreva a nossa base de conhecimentos e mantenha-se atualizado sobre os conhecimentos do sector das TI.

    Sobre nós

    The Codest - Empresa internacional de desenvolvimento de software com centros tecnológicos na Polónia.

    Reino Unido - Sede

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

    Polónia - Pólos tecnológicos locais

    • Parque de escritórios Fabryczna, Aleja
      Pokoju 18, 31-564 Cracóvia
    • Embaixada do Cérebro, Konstruktorska
      11, 02-673 Varsóvia, Polónia

      The Codest

    • Início
    • Sobre nós
    • Serviços
    • Case Studies
    • Saber como
    • Carreiras
    • Dicionário

      Serviços

    • Aconselhamento
    • Desenvolvimento de software
    • Desenvolvimento backend
    • Desenvolvimento de front-end
    • Staff Augmentation
    • Programadores de back-end
    • Engenheiros de nuvem
    • Engenheiros de dados
    • Outros
    • Engenheiros de GQ

      Recursos

    • Factos e mitos sobre a cooperação com um parceiro externo de desenvolvimento de software
    • Dos EUA para a Europa: Porque é que as empresas americanas decidem mudar-se para a Europa?
    • Comparação dos centros de desenvolvimento da Tech Offshore: Tech Offshore Europa (Polónia), ASEAN (Filipinas), Eurásia (Turquia)
    • Quais são os principais desafios dos CTOs e dos CIOs?
    • The Codest
    • The Codest
    • The Codest
    • Privacy policy
    • Website terms of use

    Direitos de autor © 2026 por The Codest. Todos os direitos reservados.

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