5 eksempler på hvordan Ruby kan brukes på best mulig måte
Har du noen gang lurt på hva vi kan gjøre med Ruby? Det er nok ingen grenser, men vi snakker gjerne om noen mer eller mindre kjente tilfeller...
Vi skal lage en bokhylle-app som viser bøker med (eller uten) forfatterdata.
Vi skal lage en bokhylle-app som viser bøker med (eller uten) forfatterdata. Det vil være en enkelt #index
action og noen frø. Dette vil være et eksempel på en app som viser hvordan du kan gi en bruker kontroll over inkluderte underressurser i et REST-aktig API.
inkluderer
spørringsparameter for å laste inn tilknyttede ressurser (forfatter
).inkluderer
har spørringsparameteren formatet string: kommaseparerte ord som representerer nestede ressurser.Vi vil bruke blåskriver
som serializer, fordi den er formatuavhengig og ganske fleksibel. Dette er den eneste perlen vi vil legge til i Rails' standardverktøysett.
La oss lage et eksempel på en app. Vi legger ikke til et testrammeverk, siden det er utenfor vårt område.
skinner ny bokhylle -T
Opprett nå Forfatter
modell:
rails g modell forfatternavn:string
#=> invoke active_record
#=> create db/migrate/20211224084524_create_authors.rb
#=> create app/models/author.rb
Og Bok
:
rails g model book author:referanser title:string
# => invoke active_record
# => create db/migrate/20211224084614_create_books.rb
# => create app/models/book.rb
Vi trenger noen frø:
# db/seeds.rb
dumas = Author.create(navn: 'Alexandre Dumas')
lewis = Author.create(navn: 'C.S. Lewis')
martin = Author.create(navn: 'Robert C. Martin')
Book.create(forfatter: dumas, tittel: 'De tre musketerer')
Book.create(forfatter: lewis, tittel: 'Løven, heksa og klesskapet')
Book.create(forfatter: martin, tittel: 'Clean Code')
Og nå er vi klare til å kjøre migreringer og seed db-en:
rails db:migrate && rails db:seed
La oss legge til har_mange
for bøker i Forfatter
modell:
# app/models/author.rb
class Forfatter < ApplicationRecord
has_many :books
end
Det er på tide å skrive en kontroller som returnerer dataene våre. Vi vil bruke API
navnerom, så la oss først legge til et akronym til bøyninger:
# config/initializers/inflections.rb
ActiveSupport::Inflector.inflections(:en) do |inflect|
inflect.akronym 'API'
end
Ok, la oss legge til serialisatoren vår til Gemfile
:
# Legg til i Gemfile
gem 'blueprinter'
Og selvfølgelig installere det:
pakkeinstallasjon
Så kan vi bygge våre egne tegninger:
# app/blueprints/author_blueprint.rb
class AuthorBlueprint < Blueprinter::Base
identifikator :id
fields :name
end
# app/blueprints/book_blueprint.rb
class BookBlueprint < Blueprinter::Base
identifikator :id
fields :title
association :author, blueprint: AuthorBlueprint
end
Legg til en basekontroller for API
:
# app/controllers/api/v1/base_controller.rb
modul API
modul V1
class BaseController < ActionController::API
end
end
end
Og utkastet til versjonen av vår BooksController
:
# app/controllers/api/v1/books_controller.rb
modul API
modul V1
class BooksController < BaseController
def index
books = Book.all
render json: BookBlueprint.render(bøker)
end
end
end
end
Vi må selvfølgelig også definere ruting:
# config/routes.rb
Rails.application.routes.draw do
namespace :api do
namespace :v1 do
resources :books, only: :index
end
end
end
La oss teste det vi har gjort så langt:
skinner s
krølle http://localhost:3000/api/v1/books
# => [{"id":1,"author":{"id":1,"name":"Alexandre Dumas"},"title":"De tre musketerer"},{"id":2,"author":{"id":2,"name":"C. S. Lewis"},"title":"Løven, heksa og klesskapet"},{"id":3,"author":{"id":3,"name":"C.S. Lewis"},"title":"Løven, heksa og klesskapet"},{"id":3,"author":{"id":3,"name":"Robert C. Martin"},"title":"Clean Kode"}]
Dataene ser ut til å være i orden, men hva med loggene?
#-forespørselslogger (n+1)
Startet GET "/api/v1/books" for 127.0.0.1 kl 2021-12-24 10:19:40 +0100
Behandlet av API::V1::BooksController#index som */*
Bokinnlasting (0,1 ms) SELECT "books".* FROM "books"
↳ app/controllers/api/v1/books_controller.rb:7:in `index'
Author Load (0.1ms) SELECT "authors".* FROM "authors" WHERE "authors"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]]
↳ app/controllers/api/v1/books_controller.rb:7:in `index'
Author Load (0.1ms) SELECT "authors".* FROM "authors" WHERE "authors"."id" = ? LIMIT ? [["id", 2], ["LIMIT", 1]]]
↳ app/controllers/api/v1/books_controller.rb:7:in `index'
Author Load (0.1ms) SELECT "authors".* FROM "authors" WHERE "authors"."id" = ? LIMIT ? [["id", 3], ["LIMIT", 1]]]
↳ app/controllers/api/v1/books_controller.rb:7:in `index'
Fullført 200 OK på 6 ms (Views: 0,1 ms | ActiveRecord: 0,4 ms | Allokeringer: 3134)
Ved å bruke assosiasjon i serialisatorene våre introduserte vi n+1
problem. Vi ønsker å eliminere det ved å gi brukeren kontroll over hva han ber om i dette endepunktet. Så han bør enten kunne laste inn bare bøker, eller sende inn parameteren includes og få forfattere i tillegg, men helst uten n+1
.
La oss definere en konstant som vil holde informasjon om hvilke assosiasjoner til bøker brukeren kan inkludere i bøker#index
handling:
# lib/constants/books/includes.rb
modul Konstanter
modul Books
modul Inkluderer
ALLOWED = {
index: %i[
forfatter
].freeze
}.freeze
slutt
slutt
slutt
Deretter definerer vi et navneområde for tomme objektkonstanter:
# lib/constants/empty.rb
modul Konstanter
modul Empty
HASH = {}.freeze
end
slutt
Og her er hovedtjenesten vår for tillatelser. Jeg tror koden er ganske selvforklarende, noen deler av magi
er kun allokert i #standard_ressursnøkkel
og #standard_formål
. Disse metodene er definert slik at vi kan kalle permit includes ved å sende bare params i rails' kontrollere. Utdataene vil være hashen som lagrer ekte
for hver tillatt inkludering.
# app/services/permit_includes.rb
require 'constants/empty'
require 'constants/books/includes'
klasse PermitIncludes
Empty = Konstanter::Empty
COMMA = ','
SLASH = '/'
INCLUDES_FORMAT = /A[a-z]+(,[a-z]+)*z/.freeze
ALLOWED_INCLUDES = {
books: 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)
slutt
privat
def default_resources_key(params)
raise(ArgumentError, 'params :controller-nøkkel må være en streng') unless params[:controller].is_a?(String)
params[:controller].split(SLASH).last&.to_sym
end
def default_purpose(params)
raise(ArgumentError, 'params :action-nøkkelen må være en streng') 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 med mindre 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
end
Nå må vi bruke nøklene til å laste inn includes og sende selve inlcudes-hashingen til serialisereren:
# app/controllers/api/v1/books_controller.rb
modul API
modul V1
class BooksController < BaseController
def index
includes = PermitIncludes.new.call(params)
books = Book.includes(includes.keys).all
render json: BookBlueprint.render(books, includes: includes)
end
end
end
end
Og det er slik vi må tilpasse serialisereren vår - vi laster bare inn assosiasjonen hvis den er inkludert:
# app/blueprints/book_blueprint.rb
class BookBlueprint (_field_name, _book, options) {
options[:includes] && options[:includes][:author]
}
end
La oss teste det igjen:
skinner s
curl http://localhost:3000/api/v1/books
# => [{"id":1,"title":"De tre musketerer"},{"id":2,"title":"Løven, heksa og klesskapet"},{"id":3,"title":"Clean Code"}]
#-forespørselslogger (vi laster bare inn bøker)
Startet GET "/api/v1/books" for ::1 kl 2021-12-24 10:33:41 +0100
Behandlet av API::V1::BooksController#index som */*
(0.1ms) SELECT sqlite_version(*)
↳ app/controllers/api/v1/books_controller.rb:8:in `index'
Bokinnlasting (0.1ms) SELECT "books".* FROM "books"
↳ app/controllers/api/v1/books_controller.rb:8:in `index'
Fullført 200 OK på 9ms (Views: 0,1ms | ActiveRecord: 0,9ms | Allokeringer: 4548)
Bra, vi har ikke bestått inkluderingen, så vi fikk bare bøker uten forfattere. La oss nå be om dem:
curl 'http://localhost:3000/api/v1/books?includes=author'
# => [{"id":1,"author":{"id":1,"name":"Alexandre Dumas"},"title":"De tre musketerer"},{"id":2,"author":{"id":2,"name":"C.S. Lewis"},"title":"Løven, heksa og klesskapet"},{"id":3,"author":{"id":3,"name":"C.S. Lewis"},"title":"Løven, heksa og klesskapet"},{"id":3,"author":{"id":3,"name":"Robert C. Martin"},"title":"Clean Code"}]%
#-forespørselslogger (eliminert n+1)
Startet GET "/api/v1/books?includes=author" for ::1 kl 2021-12-24 10:38:23 +0100
Behandlet av API::V1::BooksController#index som */*
Parametere: {"includes"=>"author"}
Bokinnlasting (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]] [["id", 1], ["id", 2], ["id", 3]]
↳ app/controllers/api/v1/books_controller.rb:8:in `index'
Fullført 200 OK på 17 ms (Visninger: 0,1 ms | ActiveRecord: 0,7 ms | Allokeringer: 7373)
Kult! Vi fikk foreningen lastet og eliminert n+1
problem. Tjenesten kan brukes for alle ressurser, alt vi ønsker å gjøre er å legge til tillatte konstanter i riktig format og legge dem til PermitIncludes::ALLOWED_INCLUDES
.
Vi må huske at dette sannsynligvis bør brukes med paginering (og forsiktighet) fordi det å inkludere assosiasjoner kan "spise" mye minne.