5 esempi di utilizzo ottimale di Ruby
Vi siete mai chiesti cosa possiamo fare con Ruby? Beh, il cielo è probabilmente il limite, ma siamo felici di parlare di alcuni casi più o meno noti...
Costruiremo un'applicazione libreria per elencare i libri con (o senza) i dati degli autori.
Costruiremo un'applicazione libreria per elencare i libri con (o senza) i dati degli autori. Ci sarà un singolo #indice
e alcuni semi. Si tratta di un'applicazione di esempio per mostrare come si possa dare all'utente il controllo sulle azioni incluse nel file. risorse secondarie in un'API di tipo REST.
comprende
per caricare le risorse associate (autore
).comprende
Il parametro della query ha un formato di stringa: parole separate da virgole, che rappresentano risorse annidate.Utilizzeremo stampante blu
come serializzatore, perché è agnostico rispetto al formato e abbastanza flessibile. È un'unica gemma che aggiungeremo al set di strumenti standard di rails.
Creiamo un'applicazione di esempio. Non aggiungeremo il framework di test, perché è fuori dal nostro scopo.
binari nuova libreria -T
Creare ora Autore
modello:
rails g modello autore nome:string
#=> invocare active_record
#=> creare db/migrate/20211224084524_create_authors.rb
#=> creare app/modelli/autore.rb
E Libro
:
rails g model book author:references title:string
# => invocare active_record
# => create db/migrate/20211224084614_create_books.rb
# => creare app/modelli/libri.rb
Avremo bisogno di semi:
# db/seeds.rb
dumas = Author.create(nome: 'Alexandre Dumas')
lewis = Author.create(nome: 'C.S. Lewis')
martin = Author.create(nome: 'Robert C. Martin')
Book.create(author: dumas, title: 'The Three Musketeers')
Book.create(author: lewis, title: 'The Lion, the Witch and the Wardrobe')
Book.create(author: martin, title: 'Clean Code')
Ora siamo pronti a eseguire le migrazioni e a seminare il database:
rails db:migrate && rails db:seed
Aggiungiamo ha_molti
per i libri in Autore
modello:
# app/models/author.rb
class Author < ApplicationRecord
has_many :books
fine
È il momento di scrivere un controllore che restituisca i nostri dati. Utilizzeremo API
quindi per prima cosa aggiungiamo un acronimo alle inflessioni:
# config/initializers/inflections.rb
ActiveSupport::Inflector.inflections(:en) do |inflect|
inflettere.acronimo 'API'
fine
Ok, aggiungiamo il nostro serializzatore a Profilo delle gemme
:
# Aggiungere al file delle gemme
gemma 'blueprinter'
E naturalmente installarlo:
installare il bundle
Poi possiamo costruire i nostri progetti:
# app/blueprints/author_blueprint.rb
classe AuthorBlueprint < Blueprinter::Base
identificatore :id
campi :nome
fine
# app/blueprints/book_blueprint.rb
class BookBlueprint < Blueprinter::Base
identificatore :id
campi :titolo
associazione :autore, blueprint: AuthorBlueprint
fine
Aggiungere un controllore di base per API
:
# app/controller/api/v1/base_controller.rb
modulo API
modulo V1
classe BaseController < ActionController::API
fine
fine
fine
E la versione in bozza del nostro Controllore di libri
:
# app/controller/api/v1/books_controller.rb
modulo API
modulo V1
classe BooksController < BaseController
def indice
libri = Book.all
rendere json: BookBlueprint.render(books)
fine
fine
fine
fine
Naturalmente dobbiamo anche definire il routing:
# config/routes.rb
Rails.application.routes.draw do
spazio dei nomi :api do
spazio dei nomi :v1 do
risorse :libri, solo: :index
fine
fine
fine
Verifichiamo quanto fatto finora:
rotaie s
ricciolo http://localhost:3000/api/v1/books
# => [{"id":1, "autore":{"id":1, "nome": "Alexandre Dumas"}, "titolo": "I tre moschettieri"},{"id":2, "autore":{"id":2, "nome": "C.S. Lewis"}, "titolo": "Il leone, la strega e l'armadio"},{"id":3, "autore":{"id":3, "nome": "Robert C. Martin"}, "titolo": "Clean Codice"}]
I dati sembrano essere a posto, e i log?
Registri di richiesta # (n+1)
Avviata GET "/api/v1/books" per 127.0.0.1 a 2021-12-24 10:19:40 +0100
Elaborazione da parte di API::V1::BooksController#index come */*
Caricamento dei libri (0.1ms) SELECT "books".* FROM "books"
↳ app/controllers/api/v1/books_controller.rb:7:in `index'
Caricamento degli autori (0.1ms) SELECT "authors".* FROM "authors" WHERE "authors". "id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]
↳ app/controller/api/v1/books_controller.rb:7:in `index'
Caricamento degli autori (0.1ms) SELECT "authors".* FROM "authors" WHERE "authors". "id" = ? LIMIT ? [["id", 2], ["LIMIT", 1]]
↳ app/controller/api/v1/books_controller.rb:7:in `index'
Caricamento degli autori (0.1ms) SELECT "authors".* FROM "authors" WHERE "authors". "id" = ? LIMIT ? [["id", 3], ["LIMIT", 1]]
↳ app/controllers/api/v1/books_controller.rb:7:in `index'
Completato 200 OK in 6ms (Views: 0.1ms | ActiveRecord: 0.4ms | Allocations: 3134)
Utilizzando l'associazione nei nostri serializzatori, abbiamo introdotto il concetto di n+1
problema. Vogliamo eliminarlo aggiungendo all'utente un controllo su ciò che richiede in questo endpoint. Quindi dovrebbe essere in grado di caricare solo i libri, oppure di passare il parametro includes e ottenere anche gli autori, ma preferibilmente senza il parametro n+1
.
Definiamo una costante che mantenga un'informazione su quali assocs di libri l'utente può includere in libri#indice
azione:
# lib/constants/books/includes.rb
modulo Costanti
modulo Libri
modulo Include
CONSENTITO = {
indice: %i[
autore
].freeze
}.freeze
fine
fine
fine
Si definisce quindi uno spazio dei nomi per le costanti degli oggetti vuoti:
# lib/constants/empty.rb
modulo Costanti
modulo Vuoto
HASH = {}.freeze
fine
fine
Ed ecco il nostro servizio principale per consentire gli include. Penso che il codice sia abbastanza autoesplicativo, alcuni pezzi di magia
sono allocati solo in #chiave_risorse_predefinite
e #scopo_predefinito
. Questi metodi sono definiti per consentirci di chiamare i permessi include passando solo i params nei controllori di rails. L'output sarà l'hash che memorizza vero
per ogni inclusione consentita.
# app/services/permit_includes.rb
require 'constants/empty'
richiede 'constants/books/includes'
classe PermitIncludes
Vuoto = Costanti::Vuoto
COMMA = ','
SLASH = '/'
INCLUDE_FORMAT = /A[a-z]+(,[a-z]+)*z/.freeze
ALLOWED_INCLUDES = {
libri: 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)
requested_includes = parse_includes(params)
allowed_includes = filter_includes(requested_includes, resources, purpose)
allowed_includes.index_with(true)
fine
privato
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
fine
def default_purpose(params)
raise(ArgumentError, 'params :action key must be a string') unless params[:action].is_a?(String)
params[:action].to_sym
fine
def include_sent?(params)
params.key?(:includes)
fine
def includes_valid?(params)
return false unless params[:includes].is_a?(String)
params[:includes].match?(INCLUDES_FORMAT)
fine
def parse_includes(params)
params[:includes].split(COMMA).map(&:to_sym)
fine
def filter_includes(requested_includes, resources_key, purpose)
requested_includes & ALLOWED_INCLUDES[resources_key][purpose]
fine
fine
Ora dobbiamo usare le chiavi per caricare gli include e passare l'hash degli include stessi al serializzatore:
# app/controller/api/v1/books_controller.rb
modulo API
modulo V1
classe BooksController < BaseController
def index
include = PermitIncludes.new.call(params)
libri = Book.includes(includes.keys).all
renderizzare json: BookBlueprint.render(books, includes: includes)
fine
fine
fine
fine
Ed è così che dobbiamo modificare il nostro serializzatore: carichiamo l'associazione solo se è inclusa:
# app/blueprints/book_blueprint.rb
class BookBlueprint (_nome_campo, _libro, opzioni) {
options[:includes] && options[:includes][:author]
}
end
Eseguiamo un altro test:
rotaie s
curl http://localhost:3000/api/v1/books
# => [{"id":1, "titolo": "I tre moschettieri"},{"id":2, "titolo": "Il leone, la strega e l'armadio"},{"id":3, "titolo": "Codice pulito"}]
Log delle richieste # (carichiamo solo libri)
Avviata GET "/api/v1/books" per ::1 a 2021-12-24 10:33:41 +0100
Elaborazione da parte di API::V1::BooksController#index come */*
(0,1 ms) SELEZIONA sqlite_version(*)
↳ app/controllers/api/v1/books_controller.rb:8:in `index'
Caricamento dei libri (0.1ms) SELECT "books".* FROM "books"
↳ app/controllers/api/v1/books_controller.rb:8:in `index'
Completato 200 OK in 9ms (Views: 0.1ms | ActiveRecord: 0.9ms | Allocations: 4548)
Bene, non abbiamo superato gli include e quindi abbiamo solo libri, senza autori. Ora richiediamoli:
curl 'http://localhost:3000/api/v1/books?includes=author'
# => [{"id":1, "autore":{"id":1, "nome": "Alexandre Dumas"}, "titolo": "I tre moschettieri"},{"id":2, "autore":{"id":2, "nome": "C.S. Lewis"}, "titolo": "Il leone, la strega e l'armadio"},{"id":3, "autore":{"id":3, "nome": "Robert C. Martin"}, "titolo": "Codice pulito"}]%
Registri di richiesta # (eliminati n+1)
Avviata GET "/api/v1/books?includes=author" per ::1 al 2021-12-24 10:38:23 +0100
Elaborazione da parte di API::V1::BooksController#index come */*
Parametri: {"includes"=>"author"}
Caricamento dei libri (0,1 ms) 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'
Completato 200 OK in 17ms (Views: 0.1ms | ActiveRecord: 0.7ms | Allocations: 7373)
Forte! L'associazione è stata caricata ed eliminata n+1
problema. Il servizio può essere usato per qualsiasi risorsa, tutto ciò che vogliamo fare è aggiungere le costanti consentite nel formato corretto e aggiungerle a PermitIncludes::ALLOWED_INCLUDES
.
Bisogna ricordare che questo dovrebbe essere usato probabilmente con la paginazione (e con cautela), perché l'inclusione di associazioni può "mangiare" molta memoria.