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...
Vamos criar uma aplicação de estante para listar livros com (ou sem) dados de autores.
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.
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.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.
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.