5 eksempler på den bedste brug af Ruby
Har du nogensinde undret dig over, hvad vi kan gøre med Ruby? Det er nok kun fantasien, der sætter grænser, men vi fortæller gerne om nogle mere eller mindre kendte tilfælde...
Vi vil bygge en bogreol-app til at liste bøger med (eller uden) forfatterdata.
Vi vil bygge en bogreol-app til at liste bøger med (eller uden) forfatterdata. Der vil være en enkelt #index
action og nogle seeds. Dette vil være et eksempel på en app, der viser, hvordan du kan give en bruger kontrol over inkluderede underressourcer i en REST-agtig API.
omfatter
forespørgselsparameter for at indlæse tilknyttede ressourcer (forfatter
).omfatter
Forespørgselsparameteren har formatet string: kommaseparerede ord, der repræsenterer indlejrede ressourcer.Vi vil bruge blåprinter
som serializer, fordi den er format-agnostisk og ret fleksibel. Dette er den eneste perle, vi vil tilføje til rails' standardværktøjssæt.
Lad os skabe et eksempel på en app. Vi tilføjer ikke et testframework, da det er uden for vores område.
skinner ny bogreol -T
Opret nu Forfatter
model:
rails g model forfatternavn:string
#=> invoke active_record
#=> opret db/migrate/20211224084524_create_authors.rb
#=> opret app/models/author.rb
Og Bog
:
rails g model book author:references title:string
# => påkald aktiv_rekord
# => opret db/migrate/20211224084614_create_books.rb
# => opret app/models/book.rb
Vi får brug for nogle 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, titel: 'De tre musketerer')
Book.create(author: lewis, title: 'Løven, heksen og garderobeskabet')
Book.create(forfatter: martin, titel: 'Clean Code')
Og nu er vi klar til at køre migreringer og sætte db'en i gang:
rails db:migrate && rails db:seed
Lad os tilføje har_mange
for bøger i Forfatter
model:
# app/models/author.rb
class Author < ApplicationRecord
has_many :bøger
end
Det er tid til at skrive en controller, der returnerer vores data. Vi vil bruge API
namespace, så lad os først tilføje et akronym til bøjninger:
# config/initializers/inflections.rb
ActiveSupport::Inflector.inflections(:en) do |inflect|
inflect.akronym 'API'
end
Okay, lad os tilføje vores serializer til Gemfile
:
# Tilføj til Gemfile
gem 'blueprinter'
Og selvfølgelig installere den:
installation af bundt
Så kan vi bygge vores tegninger:
# app/blueprints/author_blueprint.rb
class AuthorBlueprint < Blueprinter::Base
identifikator :id
felter :navn
end
# app/blueprints/book_blueprint.rb
class BookBlueprint < Blueprinter::Base
identifikator :id
felter :title
association :author, blueprint: ForfatterBlueprint
slutning
Tilføj en basecontroller til API
:
# app/controllers/api/v1/base_controller.rb
modul API
modul V1
class BaseController < ActionController::API
end
slut
slut
Og udkastet til vores BooksController
:
# app/controllers/api/v1/books_controller.rb
modul API
modul V1
class BooksController < BaseController
def index
bøger = Book.all
render json: BookBlueprint.render(bøger)
slut
slut
slut
slut
Vi skal selvfølgelig også definere routing:
# config/routes.rb
Rails.application.routes.draw do
namespace :api do
namespace :v1 do
ressourcer :bøger, kun: :indeks
end
end
slut
Lad os teste, hvad vi har gjort indtil nu:
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, heksen og garderobeskabet"},{"id":3,"author":{"id":3,"name":"The Lion, Witch and Wardrobe"}.S. Lewis"},"title":"Løven, heksen og garderobeskabet"},{"id":3,"author":{"id":3,"name":"Robert C. Martin"},"title":"Clean Kode"}]
Dataene ser ud til at være i orden, men hvad med logfilerne?
#-anmodningslogfiler (n+1)
Startede GET "/api/v1/books" for 127.0.0.1 kl. 2021-12-24 10:19:40 +0100
Behandlet af API::V1::BooksController#index som */*
Indlæsning af bøger (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'
Afsluttet 200 OK på 6 ms (Views: 0,1 ms | ActiveRecord: 0,4 ms | Allokeringer: 3134)
Ved at bruge association i vores serializers introducerede vi n+1
problem. Vi ønsker at eliminere det ved at give brugeren kontrol over, hvad han anmoder om i dette endpoint. Så han skal enten kun kunne indlæse bøger eller sende includes-parameteren og også få forfattere, men helst uden n+1
.
Lad os definere en konstant, der holder styr på, hvilke associationer af bøger brugeren kan inkludere i bøger#index
handling:
# lib/constants/books/includes.rb
modul Konstanter
modul Bøger
modul Inkluderer
ALLOWED = {
index: %i[
forfatter
].freeze
}.freeze
slut
slut
slut
Dernæst definerer vi et navneområde til tomme objektkonstanter:
# lib/constants/empty.rb
modul Konstanter
modul Tomt
HASH = {}.freeze
end
slut
Og her er vores hovedtjeneste til at tillade inkludering. Jeg tror, at koden er ret selvforklarende, nogle dele af magi
er kun tildelt i #standard_ressourcer_nøgle
og #standard_formål
. Disse metoder er defineret for at give os mulighed for at kalde permit includes, der kun sender params i rails' controllere. Outputtet vil være den hash, der gemmer ægte
for hver tilladt inklusion.
# app/services/permit_includes.rb
kræver 'constants/empty'
kræver 'constants/books/includes'
Klassen PermitIncludes
Empty = Konstanter::Empty
COMMA = ','
SLASH = '/'
INCLUDES_FORMAT = /A[a-z]+(,[a-z]+)*z/.freeze
TILLADTE_INKLUDERINGER = {
bøger: 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)
slut
privat
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
slut
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 medmindre 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
Nu skal vi bruge nøglerne til at indlæse includes og sende selve inlcudes-hash'en til serializeren:
# app/controllers/api/v1/books_controller.rb
modul API
modul V1
class BooksController < BaseController
def index
includes = PermitIncludes.new.call(params)
bøger = Book.includes(includes.keys).all
render json: BookBlueprint.render(books, includes: includes)
slut
slut
slut
slut
Og det er sådan, vi skal tilpasse vores serializer - vi indlæser kun associationen, hvis den er inkluderet:
# app/blueprints/book_blueprint.rb
class BookBlueprint (_field_name, _book, options) {
options[:includes] && options[:includes][:author]
}
end
Lad os teste det igen:
Skinner s
krølle 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"}].
# request logs (vi indlæser kun bøger)
Startede GET "/api/v1/books" for ::1 kl. 2021-12-24 10:33:41 +0100
Behandlet af API::V1::BooksController#index som */*
(0.1ms) SELECT sqlite_version(*)
↳ app/controllers/api/v1/books_controller.rb:8:in `index'
Indlæsning af bøger (0,1 ms) SELECT "books".* FROM "books"
↳ app/controllers/api/v1/books_controller.rb:8:in `index'
Afsluttet 200 OK på 9ms (Views: 0.1ms | ActiveRecord: 0.9ms | Allocations: 4548)
Godt, vi har ikke bestået inkluderingen, så vi fik kun bøger uden forfattere. Lad os nu anmode 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, heksen og garderobeskabet"},{"id":3,"author":{"id":3,"name":"The Lion, Witch and Wardrobe"}. Lewis"},"title":"Løven, heksen og garderobeskabet"},{"id":3,"author":{"id":3,"name":"Robert C. Martin"},"title":"Clean Code"}]%
#-anmodningslogfiler (elimineret n+1)
Startede GET "/api/v1/books?includes=author" for ::1 at 2021-12-24 10:38:23 +0100
Behandlet af API::V1::BooksController#index som */*
Parametre: {"includes"=>"author"}
Indlæsning af bøger (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'
Afsluttet 200 OK på 17 ms (Views: 0,1 ms | ActiveRecord: 0,7 ms | Allokeringer: 7373)
Sejt! Vi fik foreningen indlæst og elimineret n+1
problem. Tjenesten kan bruges til enhver ressource, og det eneste, vi vil gøre, er at tilføje tilladte konstanter i det rigtige format og tilføje dem til PermitIncludes::ALLOWED_INCLUDES
.
Vi skal huske, at dette sandsynligvis skal bruges med paginering (og forsigtighed), fordi inkludering af associationer kan "spise" en masse hukommelse.