5 voorbeelden van het beste gebruik van Ruby
Heb je je ooit afgevraagd wat we met Ruby kunnen doen? Nou, de sky is waarschijnlijk de limit, maar we praten graag over een aantal min of meer bekende gevallen...
We zullen een boekenplank-app bouwen om boeken op te sommen met (of zonder) auteursgegevens.
We zullen een boekenplank-app bouwen om boeken met (of zonder) auteursgegevens op te sommen. Er zal een enkele #index
actie en wat seeds. Dit zal een voorbeeld app zijn om te laten zien hoe je een gebruiker controle kunt geven over opgenomen subbronnen in een REST-achtige API.
omvat
query parameter om geassocieerde bronnen te laden (auteur
).omvat
query parameter heeft een formaat van string: door komma's gescheiden woorden, die geneste bronnen voorstellen.We gebruiken blauwdruk
als een serializer, omdat het formaat agnostisch en vrij flexibel is. Dit is een juweeltje dat we zullen toevoegen aan de standaard toolset van rails.
Laten we een voorbeeldapp maken. We voegen geen testraamwerk toe omdat dat buiten ons bereik valt.
rails nieuwe boekenplank -T
Maak nu Auteur
model:
rails g model auteursnaam:string
#=> active_record oproepen
#=> maak db/migrate/20211224084524_creëer_auteurs.rb
#=> app/modellen/auteur.rb aanmaken
En Boek
:
rails g model boek auteur:referenties titel:string
# => active_record oproepen
# => aanmaken db/migrate/20211224084614_create_books.rb
# => app/modellen/boek.rb aanmaken
We hebben zaden nodig:
# db/zaden.rb
dumas = Auteur.create(naam: 'Alexandre Dumas')
lewis = Auteur.create(naam: 'C.S. Lewis')
martin = Auteur.create(naam: 'Robert C. Martin')
Boek.create(auteur: dumas, titel: 'De drie musketiers')
Boek.create(auteur: lewis, titel: 'De leeuw, de heks en de kleerkast')
Boek.create(auteur: martin, titel: "Schone code")
En nu zijn we klaar om migraties uit te voeren en de db te vullen:
rails db:migrate && rails db:seed
Laten we toevoegen heeft_veel
voor boeken in Auteur
model:
# app/modellen/author.rb
klasse Auteur < Toepassingsregistratie
heeft_veel :boeken
einde
Het is tijd om een controller te schrijven die onze gegevens retourneert. We gebruiken API
namespace, dus laten we eerst een acroniem toevoegen aan verbuigingen:
# config/initializers/inflections.rb
ActiveSupport::Inflector.inflections(:en) do |inflect|
inflect.acroniem 'API
einde
Oké, laten we onze serializer toevoegen aan Gemfile
:
# Toevoegen aan Gemfile
Gem 'blueprinter
En natuurlijk installeren:
bundel installeren
Dan kunnen we onze blauwdrukken maken:
# app/blueprints/author_blueprint.rb
klasse AuteurBlueprint < Blueprinter::Base
identificator :id
velden :naam
einde
# app/blueprints/boek_blauwdruk.rb
klasse Boekblauwdruk < Blauwdruk::Basis
identificator :id
velden :title
associatie :auteur, blauwdruk: AuteurBlueprint
einde
Een basiscontroller toevoegen voor API
:
# app/controllers/api/v1/base_controller.rb
module API
module V1
klasse BaseController < ActionController::API
einde
einde
einde
En de conceptversie van onze BoekenController
:
# app/controllers/api/v1/books_controller.rb
module API
module V1
klasse Boekencontroller < Basiscontroller
def index
boeken = Boek.alle
json weergeven: BookBlueprint.render(books)
einde
einde
einde
einde
We moeten natuurlijk ook routing definiëren:
# config/routes.rb
Rails.application.routes.draw doen
naamruimte :api doen
naamruimte :v1 doen
bronnen :boeken, alleen: :index
einde
einde
einde
Laten we testen wat we tot nu toe hebben gedaan:
rails s
krul http://localhost:3000/api/v1/books
# => [{"id":1,"auteur":{"id":1,"naam":"Alexandre Dumas"},"titel":"De drie musketiers"},{"id":2,"auteur":{"id":2,"naam":"C.S. Lewis"},"titel":"De leeuw, de heks en de kleerkast"},{"id":3,"auteur":{"id":3,"naam":"Robert C. Martin"},"titel":"Schone Code"}]
De gegevens lijken in orde, maar hoe zit het met de logs?
# verzoeklogs (n+1)
Gestart met GET "/api/v1/books" voor 127.0.0.1 op 2021-12-24 10:19:40 +0100
Verwerking door API::V1::BooksController#index als */*
Boeken laden (0.1ms) SELECT "books".* FROM "books".
app/controllers/api/v1/books_controller.rb:7:in `index'.
Auteur laden (0.1ms) SELECT "authors".* FROM "authors" WHERE "authors"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]
app/controllers/api/v1/books_controller.rb:7:in `index'.
Auteur laden (0.1ms) SELECT "authors".* FROM "authors" WHERE "authors"."id" = ? LIMIT ? [["id", 2], ["LIMIT", 1]]
↳ app/controllers/api/v1/books_controller.rb:7:in `index'
Auteur laden (0.1ms) SELECT "authors".* FROM "authors" WHERE "authors"."id" = ? LIMIT ? [["id", 3], ["LIMIT", 1]]
↳ app/controllers/api/v1/books_controller.rb:7:in `index'
Voltooid 200 OK in 6ms (Views: 0.1ms | ActiveRecord: 0.4ms | Toewijzingen: 3134)
Door associatie te gebruiken in onze serializers hebben we n+1
probleem. We willen het elimineren door de gebruiker controle te geven over wat hij opvraagt in dit eindpunt. Dus hij moet in staat zijn om ofwel alleen boeken te laden, of de includes parameter door te geven en ook auteurs te krijgen, maar bij voorkeur zonder de n+1
.
Laten we een constante definiëren die informatie bijhoudt over welke associs van boeken de gebruiker kan opnemen in boeken#index
actie:
# lib/constants/bookss/includes.rb
module Constanten
module Boeken
module Inclusief
TOEGESTAAN = {
index: %i[
auteur
].bevriezen
}.bevriezen
einde
einde
einde
Vervolgens definiëren we een naamruimte voor lege objectconstanten:
# lib/constants/empty.rb
module Constanten
module Leeg
HASH = {}.freeze
einde
einde
En hier is onze hoofdservice voor het toestaan van includes. Ik denk dat de code vrij voor zichzelf spreekt, sommige stukken van magie
worden alleen toegewezen in #standaard_bronnen_sleutel
en #standaard_doel
. Deze methodes zijn gedefinieerd zodat we permit includes kunnen aanroepen door alleen params door te geven in de controllers van rails. De uitvoer zal de hash zijn die Echt
voor elke toegestane opname.
# app/services/permit_includes.rb
require 'constants/empty
require 'constants/books/includes
klasse ToestemmingInclusies
Leeg = Constanten::Leeg
COMMA = ','
SLASH = '/'
INCLUDES_FORMAT = /A[a-z]+(,[a-z]+)*z/.freeze
TOEGESTAAN_INCLUDES = {
boeken: Constants::Books::Includes::ALLOWED
}.bevriezen
def call(params, resources: default_resources_key(params), purpose: default_purpose(params))
return Empty::HASH tenzij includes_sent?(params)
return Empty::HASH tenzij includes_valid?(params)
aangevraagde_includes = parse_includes(params)
allowed_includes = filter_includes(requested_includes, resources, purpose)
allowed_includes.index_with(true)
einde
privé
def default_resources_key(params)
raise(ArgumentError, 'params :controller key moet een string zijn') tenzij params[:controller].is_a?(String)
params[:controller].split(SLASH).last&.to_sym
einde
def default_purpose(params)
raise(ArgumentError, 'params :action key moet een string zijn') tenzij params[:action].is_a?(String)
params[:actie].naar_sym
einde
def omvat_verzonden?(params)
params.key?(:includes)
einde
def omvat_geldig?(params)
return false tenzij params[:includes].is_a?(String)
params[:includes].match?(INCLUDES_FORMAT)
einde
def parse_includes(params)
params[:includes].split(COMMA).map(&:to_sym)
einde
def filter_includes(requested_includes, resources_key, doel)
requested_includes & ALLOWED_INCLUDES[resources_key][purpose]
einde
einde
Nu moeten we de sleutels gebruiken om includes te laden en de inlcudes hash zelf doorgeven aan de serializer:
# app/controllers/api/v1/books_controller.rb
module API
module V1
klasse Boekencontroller < Basiscontroller
def index
includes = PermitIncludes.new.call(params)
boeken = Boek.bevat(includes.keys).alle
json renderen: BookBlueprint.render(books, includes: includes)
einde
einde
einde
einde
En dit is hoe we onze serializer moeten aanpassen - we laden de associatie alleen als deze is opgenomen:
# app/blueprints/boek_blauwdruk.rb
klasse Boekblauwdruk (_veld_naam, _boek, opties) {
options[:includes] && options[:includes][:author]
}
einde
Laten we het nog eens testen:
rails s
krul http://localhost:3000/api/v1/books
# => [{"id":1,"title":"The Three Musketeers"},{"id":2,"title":"The Lion, the Witch and the Wardrobe"},{"id":3,"title":"Clean Code"}]
# verzoeklogs (we laden alleen boeken)
Gestart met GET "/api/v1/books" voor ::1 op 2021-12-24 10:33:41 +0100
Verwerking door API::V1::BooksController#index als */*
(0.1ms) SELECT sqlite_version(*)
↳ app/controllers/api/v1/books_controller.rb:8:in `index'.
Boek laden (0.1ms) SELECT "books".* FROM "books".
↳ app/controllers/api/v1/books_controller.rb:8:in `index'
Voltooid 200 OK in 9 ms (Views: 0.1ms | ActiveRecord: 0.9ms | Toewijzingen: 4548)
Goed, we zijn nog niet voorbij de includes dus we hebben alleen boeken, zonder auteurs. Laten we ze nu opvragen:
krul 'http://localhost:3000/api/v1/books?includes=author'
# => [{"id":1,"auteur":{"id":1,"naam":"Alexandre Dumas"},"titel":"De drie musketiers"},{"id":2,"auteur":{"id":2,"naam":"C.S. Lewis"},"titel":"De leeuw, de heks en de kleerkast"},{"id":3,"auteur":{"id":3,"naam":"Robert C. Martin"},"titel":"Schone code"}]%
# verzoeklogs (geëlimineerd n+1)
Gestart met GET "/api/v1/books?includes=author" voor ::1 op 2021-12-24 10:38:23 +0100
Verwerking door API::V1::BooksController#index als */*
Parameters: {"includes"=>"author"}
Boek laden (0.1ms) SELECT "books".* FROM "books".
app/controllers/api/v1/books_controller.rb:8:in `index'.
Auteur laden (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'
Voltooid 200 OK in 17ms (Views: 0.1ms | ActiveRecord: 0.7ms | Toewijzingen: 7373)
Gaaf! We hebben de associatie geladen en geëlimineerd n+1
probleem. De service kan worden gebruikt voor elke bron, het enige wat we willen doen is toegestane inlcudes constanten in het juiste formaat toevoegen en ze toevoegen aan PermitIncludes::ALLOWED_INCLUDES
.
We moeten onthouden dat dit waarschijnlijk moet worden gebruikt met paginering (en voorzichtigheid) omdat het opnemen van associaties veel geheugen kan "vreten".