5 exempel på hur Ruby används på bästa sätt
Har du någonsin undrat vad vi kan göra med Ruby? Tja, himlen är förmodligen gränsen, men vi är glada att prata om några mer eller mindre kända fall ...
Vi ska bygga en app för bokhyllor som listar böcker med (eller utan) författardata.
Vi ska bygga en app för bokhyllor som listar böcker med (eller utan) författardata. Det kommer att finnas en enda #index
action och några frön. Detta kommer att vara en exempelapp för att visa hur du kan ge en användare kontroll över inkluderade underresurser i ett REST-liknande API.
inkluderar
frågeparameter för att ladda associerade resurser (författare
).inkluderar
frågeparametern har formatet sträng: kommaseparerade ord som representerar nästlade resurser.Vi kommer att använda blåtryckare
som serializer, eftersom den är formatagnostisk och ganska flexibel. Detta är en enda pärla som vi kommer att lägga till i Rails standardverktyg.
Låt oss skapa ett exempel på en app. Vi lägger inte till något testramverk eftersom det ligger utanför vårt område.
skenor ny bokhylla -T
Skapa nu Författaren
modell:
rails g modell författarnamn:sträng
#=> invoke active_record
#=> skapa db/migrate/20211224084524_create_authors.rb
#=> skapa app/modeller/författare.rb
Och Bok
:
rails g model book författare:referenser titel:sträng
# => invoke active_record
# => skapa db/migrate/20211224084614_create_books.rb
# => skapa app/modeller/bok.rb
Vi kommer att behöva några frön:
# db/seeds.rb
dumas = Författare.skapa(namn: 'Alexandre Dumas')
lewis = Författare.skapa(namn: 'C.S. Lewis')
martin = Författare.skapa(namn: 'Robert C. Martin')
Book.create(författare: dumas, titel: 'De tre musketörerna')
Book.create(författare: lewis, titel: 'Lejonet, häxan och garderoben')
Bok.skapa(författare: martin, titel: 'Clean Code')
Och nu är vi redo att köra migreringar och seed the db:
rails db:migrate && rails db:seed
Låt oss lägga till har_många
för böcker i Författaren
modell:
# app/modeller/author.rb
klass Författare < ApplicationRecord
har_många :böcker
slut
Nu är det dags att skriva en controller som returnerar våra data. Vi kommer att använda API
namnrymden, så låt oss först lägga till en akronym till böjningar:
# config/initializers/inflections.rb
ActiveSupport::Inflector.inflections(:en) do |inflect|
inflektera.akronym 'API'
slut
Ok, låt oss lägga till vår serializer till Gemfil
:
# Lägg till i Gemfile
gem 'blåskrivare'
Och naturligtvis installera den:
paketinstallation
Sedan kan vi bygga våra ritningar:
# app/blueprints/author_blueprint.rb
klass AuthorBlueprint < Blueprinter::Bas
identifierare :id
fält :namn
slut
# app/blueprints/book_blueprint.rb
klass BookBlueprint < Blåskrivare::Bas
identifierare :id
fält :title
association :författare, blueprint: AuthorBlueprint
slut
Lägg till en baskontroll för API
:
# app/controllers/api/v1/base_controller.rb
modul API
modul V1
klass BaseController < ActionController::API
slut
slut
slut
Och utkastet till version av vår BooksController
:
# app/controllers/api/v1/books_controller.rb
modul API
modul V1
klass BooksController < BasController
def index
böcker = Bok.alla
Rendera json: BookBlueprint.render(böcker)
slut
slut
slut
slut
Vi måste naturligtvis också definiera routing:
# config/routes.rb
Rails.application.routes.draw do
namnområde :api do
namnområde :v1 do
resurser :böcker, endast: :index
slut
slut
slut
Låt oss testa vad vi har gjort hittills:
räls s
curl http://localhost:3000/api/v1/books
# => [{"id":1,"author":{"id":1,"name":"Alexandre Dumas"},"title":"De tre musketörerna"},{"id":2,"author":{"id":2,"name":"C.S. Lewis"},"title":"Lejonet, häxan och garderoben"},{"id":3,"author":{"id":3,"name":"Robert C. Martin"},"title":"Clean Kod"}]
Data verkar vara bra, men hur är det med loggar?
# förfrågningsloggar (n+1)
Startade GET "/api/v1/books" för 127.0.0.1 kl 2021-12-24 10:19:40 +0100
Bearbetas av API::V1::BooksController#index som */*
Bokladdning (0,1 ms) SELECT "books".* FROM "books"
↳ app/controllers/api/v1/books_controller.rb:7:in `index'
Laddning av författare (0,1 ms) SELECT "authors".* FROM "authors" WHERE "authors"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]] [["id", 1]]
↳ app/controllers/api/v1/books_controller.rb:7:in `index'
Laddning av författare (0,1 ms) SELECT "authors".* FROM "authors" WHERE "authors"."id" = ? LIMIT ? [["id", 2], ["LIMIT", 1]] [["id", 2], ["LIMIT", 1]]
↳ app/controllers/api/v1/books_controller.rb:7:in `index'
Laddning av författare (0,1 ms) SELECT "authors".* FROM "authors" WHERE "authors"."id" = ? LIMIT ? [["id", 3], ["LIMIT", 1]] [["id", 3], ["LIMIT", 1]]
↳ app/controllers/api/v1/books_controller.rb:7:in `index'
Avslutad 200 OK på 6 ms (Views: 0,1 ms | ActiveRecord: 0,4 ms | Allokeringar: 3134)
Genom att använda association i våra serializers införde vi n+1
problem. Vi vill eliminera det genom att ge användaren kontroll över vad han begär i denna endpoint. Så han bör kunna antingen ladda bara böcker, eller skicka parametern includes och få författare också, men helst utan n+1
.
Låt oss definiera en konstant som kommer att hålla information om vilka associationer av böcker som användaren kan inkludera i böcker#index
handling:
# lib/constants/books/includes.rb
modul Konstanter
modul Böcker
modul Inkluderar
ALLOWED = {
index: %i[
författare
].freeze
}.freeze
slut
slut
slut
Därefter definierar vi ett namnområde för tomma objektkonstanter:
# lib/constants/empty.rb
modul Konstanter
modul Empty
HASH = {}.freeze
slut
slut
Och här är vår huvudtjänst för att tillåta inkluderingar. Jag tror att koden är ganska självförklarande, några bitar av magi
är endast allokerade i #standard_resursnyckel
och #förvaltat_syfte
. Dessa metoder är definierade för att vi ska kunna anropa permit includes som bara skickar params i Rails controllers. Utdata kommer att vara den hash som lagrar sant
för varje tillåten inkludering.
# 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
TILLÅTNA_INKLUDERINGAR = {
böcker::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 om inte includes_valid?(params)
requested_includes = parse_includes(params)
allowed_includes = filter_includes(requested_includes, resurser, syfte)
tillåtna_inkluderingar.index_with(true)
slut
privat
def default_resources_key(params)
raise(ArgumentError, 'params :controller-nyckel måste vara en sträng') unless params[:controller].is_a?(String)
params[:controller].split(SLASH).last&.to_sym
slut
def default_purpose(params)
raise(ArgumentError, 'params :action-nyckeln måste vara en sträng') unless params[:action].is_a?(String)
params[:åtgärd].to_sym
slut
def includes_sent?(params)
params.key?(:includes)
slut
def includes_valid?(params)
return false om inte params[:includes].is_a?(String)
params[:includes].match?(INCLUDES_FORMAT)
slut
def parse_includes(params)
params[:includes].split(COMMA).map(&:to_sym)
slut
def filter_includes(requested_includes, resources_key, purpose)
requested_includes & ALLOWED_INCLUDES[resources_key][purpose]
end
slut
Nu måste vi använda nycklarna för att ladda inkluderingar och skicka själva inlcudes-hashningen till serialiseraren:
# app/controllers/api/v1/books_controller.rb
modul API
modul V1
klass BooksController < BasController
def index
includes = PermitIncludes.new.call(params)
böcker = Bok.includes(includes.keys).all
rendera json: BookBlueprint.render(böcker, inkluderar: inkluderar)
slut
slut
slut
slut
Och det är så här vi måste justera vår serializer - vi laddar bara associationen om den ingår:
# app/blueprints/book_blueprint.rb
klass BookBlueprint (_field_name, _book, options) {
options[:includes] && options[:includes][:author]
}
slut
Låt oss testa det igen:
räls s
curl http://localhost:3000/api/v1/books
# => [{"id":1,"title":"De tre musketörerna"},{"id":2,"title":"Lejonet, häxan och garderoben"},{"id":3,"title":"Clean Code"}]
# förfrågningsloggar (vi laddar bara böcker)
Startade GET "/api/v1/books" för ::1 kl 2021-12-24 10:33:41 +0100
Bearbetas av API::V1::BooksController#index som */*
(0.1ms) SELECT sqlite_version(*)
↳ app/controllers/api/v1/books_controller.rb:8:in `index'
Bokladdning (0,1 ms) SELECT "books".* FROM "books"
↳ app/controllers/api/v1/books_controller.rb:8:in `index'
Avslutad 200 OK på 9ms (Views: 0.1ms | ActiveRecord: 0.9ms | Allocations: 4548)
Bra, vi har inte passerat inkluderingarna så vi fick bara böcker, utan författare. Låt oss nu begära dem:
curl 'http://localhost:3000/api/v1/books?includes=author'
# => [{"id":1,"author":{"id":1,"name":"Alexandre Dumas"},"title":"De tre musketörerna"},{"id":2,"author":{"id":2,"name":"C.S. Lewis"},"titel":"Lejonet, häxan och garderoben"},{"id":3,"författare":{"id":3,"namn":"Robert C. Martin"},"titel":"Clean Code"}]%
# begäran loggar (eliminerad n+1)
Startade GET "/api/v1/books?includes=author" för ::1 kl 2021-12-24 10:38:23 +0100
Bearbetas av API::V1::BooksController#index som */*
Parametrar: {"includes"=>"author {"includes"=>"author"}
Bokladdning (0,1 ms) SELECT "books".* FROM "books"
↳ app/controllers/api/v1/books_controller.rb:8:in `index'
Laddning av författare (0,2 ms) SELECT "authors".* FROM "authors" WHERE "authors"."id" IN (?, ?, ?) [["id", 1], ["id", 2], ["id", 3]]
↳ app/controllers/api/v1/books_controller.rb:8:in `index'
Avslutad 200 OK på 17 ms (Views: 0,1 ms | ActiveRecord: 0,7 ms | Allokeringar: 7373)
Coolt! Vi fick föreningen laddad och eliminerad n+1
problem. Tjänsten kan användas för vilken resurs som helst, allt vi vill göra är att lägga till tillåtna inlcudes-konstanter i rätt format och lägga till dem i PermitIncludes::ALLOWED_INCLUDES
.
Vi måste komma ihåg att detta förmodligen bör användas med paginering (och försiktighet) eftersom inkludering av associationer kan "äta" mycket minne.