5 exemples de la meilleure utilisation de Ruby
Vous êtes-vous déjà demandé ce que l'on pouvait faire avec Ruby ? Eh bien, le ciel est probablement la limite, mais nous sommes heureux de parler de quelques cas plus ou moins connus...
Nous allons créer une application de bibliothèque pour lister les livres avec (ou sans) données sur les auteurs.
Nous allons créer une application de bibliothèque pour lister les livres avec (ou sans) données sur les auteurs. Il y aura une seule application #index
et quelques graines. Il s'agit d'un exemple d'application pour montrer comment on peut donner à un utilisateur le contrôle sur des éléments inclus. sous-ressources dans une API de type REST.
comprend
pour charger les ressources associées (auteur
).comprend
a un format de chaîne : mots séparés par des virgules, représentant des ressources imbriquées.Nous utiliserons imprimante bleue
comme sérialiseur, parce qu'il est indépendant du format et assez flexible. C'est une gemme que nous ajouterons à l'ensemble des outils standards de rails.
Créons un exemple d'application. Nous n'ajouterons pas de cadre de test car cela n'entre pas dans notre champ d'application.
rails nouvelle étagère -T
Créez maintenant Auteur
modèle :
rails g model author name:string
#=> invoke active_record
#=> create db/migrate/20211224084524_create_authors.rb
#=> créer app/models/author.rb
Et Livre
:
rails g model book author:references title:string
# => invoke active_record
# => créer db/migrate/20211224084614_create_books.rb
# => créer app/models/book.rb
Nous aurons besoin de semences :
# db/seeds.rb
dumas = Author.create(name : 'Alexandre Dumas')
lewis = Author.create(nom : 'C.S. Lewis')
martin = Author.create(nom : 'Robert C. Martin')
Book.create(author : dumas, title : 'Les Trois Mousquetaires')
Book.create(author : lewis, title : 'Le Lion, la Sorcière et l'Armoire')
Book.create(author : martin, title : 'Clean Code')
Nous sommes maintenant prêts à exécuter les migrations et à ensemencer la base de données :
rails db:migrate && rails db:seed
Ajoutons a_beaucoup
pour les livres en Auteur
modèle :
# app/models/author.rb
class Author < ApplicationRecord
has_many :books
end
Il est temps d'écrire un contrôleur qui renverra nos données. Nous utiliserons API
Nous allons donc commencer par ajouter un acronyme aux inflexions :
# config/initializers/inflections.rb
ActiveSupport::Inflector.inflections(:en) do |inflect|
inflect.acronyme 'API'
end
Ok, ajoutons notre sérialiseur à Fichier de gemmes
:
# Ajouter à Gemfile
gem 'blueprinter'
Et bien sûr, l'installer :
bundle install
Nous pourrons alors élaborer nos plans :
# app/blueprints/author_blueprint.rb
class AuthorBlueprint < Blueprinter::Base
identifiant :id
champs :name
end
# app/blueprints/book_blueprint.rb
class BookBlueprint < Blueprinter::Base
identifiant :id
champs :title
association :author, blueprint : AuthorBlueprint
fin
Ajouter un contrôleur de base pour API
:
# app/controllers/api/v1/base_controller.rb
module API
module V1
classe BaseController < ActionController::API
end
fin
fin
Et la version préliminaire de notre Contrôleur de livres
:
# app/controllers/api/v1/books_controller.rb
module API
module V1
classe BooksController < BaseController
def index
books = Book.all
rendez json : BookBlueprint.render(books)
end
fin
fin
fin
Nous devons également définir le routage, bien entendu :
# config/routes.rb
Rails.application.routes.draw do
espace de noms :api do
espace de noms :v1 do
ressources :books, seulement : :index
end
end
fin
Testons ce que nous avons fait jusqu'à présent :
rails s
curl http://localhost:3000/api/v1/books
# => [{"id":1, "author":{"id":1, "name" : "Alexandre Dumas"}, "title" : "Les Trois Mousquetaires"},{"id":2, "author":{"id":2, "name" : "C.S. Lewis"}, "title" : "Le Lion, la Sorcière et l'Armoire"},{"id":3, "author":{"id":3, "name" : "Robert C. Martin"}, "title" : "Clean Code"}]
Les données semblent correctes, mais qu'en est-il des journaux ?
# journaux des demandes (n+1)
Démarrage de GET "/api/v1/books" pour 127.0.0.1 at 2021-12-24 10:19:40 +0100
Traitement par API::V1::BooksController#index en tant que */*
Chargement des livres (0.1ms) SELECT "books".* FROM "books"
↳ app/controllers/api/v1/books_controller.rb:7:in `index'
Chargement des auteurs (0.1ms) SELECT "authors".* FROM "authors" WHERE "authors". "id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]
↳ app/controllers/api/v1/books_controller.rb:7:in `index'
Chargement des auteurs (0.1ms) SELECT "auteurs".* FROM "auteurs" WHERE "auteurs". "id" = ? LIMIT ? [["id", 2], ["LIMIT", 1]]
↳ app/controllers/api/v1/books_controller.rb:7:in `index'
Chargement des auteurs (0.1ms) SELECT "auteurs".* FROM "auteurs" WHERE "auteurs". "id" = ? LIMIT ? [["id", 3], ["LIMIT", 1]]
↳ app/controllers/api/v1/books_controller.rb:7:in `index'
Terminé 200 OK en 6ms (Vues : 0.1ms | ActiveRecord : 0.4ms | Allocations : 3134)
En utilisant l'association dans nos sérialiseurs, nous avons introduit la notion de n+1
problème. Nous voulons l'éliminer en ajoutant à l'utilisateur un contrôle sur ce qu'il demande dans ce point de terminaison. Ainsi, il devrait pouvoir charger uniquement les livres, ou passer le paramètre includes et obtenir également les auteurs, mais de préférence sans le paramètre n+1
.
Définissons une constante qui gardera une information sur les assocs de livres que l'utilisateur peut inclure dans livres#index
action :
# lib/constants/books/includes.rb
module Constants
module Books
module Includes
ALLOWED = {
index : %i[
auteur
].freeze
}.freeze
fin
fin
fin
Ensuite, nous définissons un espace de noms pour les constantes d'objets vides :
# lib/constants/empty.rb
module Constants
module Empty
HASH = {}.freeze
end
fin
Et voici notre service principal pour autoriser les inclusions. Je pense que le code est assez explicite. magie
ne sont alloués que dans les #default_resources_key
et #default_purpose
. Ces méthodes sont définies pour nous permettre d'appeler des inclusions de permis en passant uniquement des paramètres dans les contrôleurs de rails. La sortie sera le hash qui stocke vrai
pour chaque inclusion autorisée.
# app/services/permit_includes.rb
require 'constants/empty'
require 'constants/books/includes'
classe PermitIncludes
Empty = Constants::Empty
COMMA = ','
SLASH = '/'
INCLUDES_FORMAT = /A[a-z]+(,[a-z]+)*z/.freeze
ALLOWED_INCLUDES = {
livres : 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)
fin
privé
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
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 includes_sent ?(params)
params.key ?(:includes)
end
def includes_valid ?(params)
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
fin
Nous devons maintenant utiliser les clés pour charger les inclusions et passer le hachage des inclusions lui-même au sérialiseur :
# app/controllers/api/v1/books_controller.rb
module API
module V1
classe BooksController < BaseController
def index
includes = PermitIncludes.new.call(params)
books = Book.includes(includes.keys).all
rendez le json : BookBlueprint.render(books, includes : includes)
fin
fin
fin
fin
C'est ainsi que nous devons adapter notre sérialiseur - nous ne chargeons l'association que si elle est incluse :
# app/blueprints/book_blueprint.rb
class BookBlueprint (_field_name, _book, options) {
options[:includes] && options[:includes][:author]
}
end
Testons-le à nouveau :
rails s
curl http://localhost:3000/api/v1/books
# => [{"id":1, "title" : "Les trois mousquetaires"},{"id":2, "title" : "Le lion, la sorcière et l'armoire"},{"id":3, "title" : "Clean Code"}]
Journal des requêtes # (nous ne chargeons que des livres)
Démarrage de GET "/api/v1/books" pour ::1 at 2021-12-24 10:33:41 +0100
Traitement par API::V1::BooksController#index en tant que */*
(0.1ms) SELECT sqlite_version(*)
↳ app/controllers/api/v1/books_controller.rb:8:in `index'
Chargement des livres (0.1ms) SELECT "books".* FROM "books"
↳ app/controllers/api/v1/books_controller.rb:8:in `index'
Terminé 200 OK en 9ms (Vues : 0.1ms | ActiveRecord : 0.9ms | Allocations : 4548)
Bien, nous n'avons pas passé les inclusions et n'avons donc obtenu que des livres, sans auteurs. Demandons-les maintenant :
curl 'http://localhost:3000/api/v1/books?includes=author'
# => [{"id":1, "author":{"id":1, "name" : "Alexandre Dumas"}, "title" : "Les Trois Mousquetaires"},{"id":2, "author":{"id":2, "name" : "C.S. Lewis"}, "titre" : "Le lion, la sorcière et l'armoire"},{"id":3, "auteur":{"id":3, "nom" : "Robert C. Martin"}, "titre" : "Clean Code"}]%
# journal des demandes (éliminé n+1)
Lancé GET "/api/v1/books?includes=author" pour ::1 at 2021-12-24 10:38:23 +0100
Traitement par API::V1::BooksController#index en tant que */*
Paramètres : {"includes"=>"author"}
Chargement des livres (0.1ms) SELECT "books".* FROM "books"
↳ app/controllers/api/v1/books_controller.rb:8:in `index'
Chargement des auteurs (0.2ms) SELECT "auteurs".* FROM "auteurs" WHERE "auteurs". "id" IN ( ?, ?, ?) [["id", 1], ["id", 2], ["id", 3]]
↳ app/controllers/api/v1/books_controller.rb:8:in `index'
Terminé 200 OK en 17ms (Vues : 0.1ms | ActiveRecord : 0.7ms | Allocations : 7373)
Cool ! L'association est chargée et éliminée n+1
problème. Le service peut être utilisé pour n'importe quelle ressource, tout ce que nous voulons faire est d'ajouter des constantes autorisées dans le format approprié et de les ajouter à PermitIncludes::ALLOWED_INCLUDES
.
Nous devons nous rappeler que cela doit être utilisé avec pagination (et prudence) parce que l'inclusion d'associations peut "manger" beaucoup de mémoire.