5 esimerkkiä Rubyn parhaasta käytöstä
Oletko koskaan miettinyt, mitä voimme tehdä Rubylla? No, taivas on luultavasti rajana, mutta puhumme mielellämme muutamista enemmän tai vähemmän tunnetuista tapauksista....
Rakennamme kirjahylly-sovelluksen, jossa luetellaan kirjoja kirjailijatietojen kanssa (tai ilman niitä).
Rakennamme kirjahylly-sovelluksen, jossa luetellaan kirjoja kirjailijatietojen kanssa (tai ilman niitä). Käytössä on yksi #index
toimintaa ja joitakin siemeniä. Tämä on esimerkkisovellus, joka näyttää, miten voit antaa käyttäjälle hallinnan sisällytetyistä alaresurssit REST-käyttöliittymässä (REST-ish API).
sisältää
kyselyparametri, jolla ladataan siihen liittyviä resursseja (kirjoittaja
).sisältää
kyselyparametrin muoto on merkkijono: pilkulla erotetut sanat, jotka edustavat sisäkkäisiä resursseja.Käytämme sinipainaja
sarjallistajana, koska se on formaatista riippumaton ja melko joustava. Tämä on ainoa helmi, jonka lisäämme railsin vakiotyökalupakettiin.
Luodaan esimerkkisovellus. Emme lisää testikehystä, koska se ei kuulu tehtäväkenttäämme.
kiskot uusi kirjahylly -T
Luo nyt Kirjoittaja
malli:
rails g malli tekijän nimi:string
#=> invoke active_record
#=> create db/migrate/20211224084524_create_authors.rb
#=> luo app/models/author.rb
Ja Kirja
:
rails g malli kirja tekijä:viitteet otsikko:merkkijono
# => invoke active_record
# => luo db/migrate/20211224084614_create_books.rb
# => luo app/models/book.rb
Tarvitsemme siemeniä:
# db/seeds.rb
dumas = Author.create(nimi: 'Alexandre Dumas')
lewis = Author.create(name: 'C.S. Lewis')
martin = Author.create(nimi: 'Robert C. Martin')
Book.create(author: dumas, title: 'Kolme muskettisoturia')
Book.create(author: lewis, title: 'Leijona, noita ja vaatekaappi')
Book.create(author: martin, title: 'Clean Code')
Nyt olemme valmiita suorittamaan migraatiot ja kylvämään tietokannan:
rails db:migrate && rails db:seed
Lisätään has_many
kirjoja varten Kirjoittaja
malli:
# app/models/author.rb
class Author < ApplicationRecord
has_many :books
end
On aika kirjoittaa ohjain, joka palauttaa tietomme. Käytämme API
nimiavaruuteen, joten lisätään ensin lyhenne taivutuksille:
# config/initializers/initializers/inflections.rb
ActiveSupport::Inflector.inflections(:fi) do |inflect|
inflect.acronym 'API'
end
Okei, lisätään sarjallistajamme tiedostoon Gemfile
:
# Lisää Gemfileen
gem 'blueprinter'
Ja tietysti asenna se:
nipun asennus
Sitten voimme rakentaa suunnitelmamme:
# app/blueprints/author_blueprint.rb
class AuthorBlueprint < Blueprinter::Base
identifier :id
kentät :name
end
# app/blueprints/book_blueprint.rb
class BookBlueprint < Blueprinter::Base
identifier :id
kentät :title
association :author, blueprint: AuthorBlueprint
end
Lisää perusohjain API
:
# app/controllers/api/v1/base_controller.rb
moduuli API
moduuli V1
class BaseController < ActionController::API
end
end
end
Ja luonnosversio meidän BooksController
:
# app/controllers/api/v1/books_controller.rb
moduuli API
moduuli V1
class KirjatController < BaseController
def index
books = Book.all
render json: json: BookBlueprint.render(books)
end
end
end
end
Meidän on tietenkin myös määriteltävä reititys:
# config/routes.rb
Rails.application.routes.draw do
namespace :api do
namespace :v1 do
resources :books, only: :index
end
end
end
Testataan, mitä olemme tehneet tähän mennessä:
kiskot s
curl http://localhost:3000/api/v1/books
# => [{"id":1, "author":{"id":1, "name": "Alexandre Dumas"}, "title": "Kolme muskettisoturia"},{"id":2, "author":{"id":2, "name": "C.S. Lewis"}, "nimi": "Leijona, noita ja vaatekaappi"},{"id":3, "kirjailija":{"id":3, "nimi": "Robert C. Martin"}, "nimi": "Puhdasta Koodi"}]
Tiedot näyttävät olevan kunnossa, entä lokit?
#-pyyntölokit (n+1)
Käynnistetty GET "/api/v1/books" osoitteessa 127.0.0.1 klo 2021-12-24 10:19:40 +0100
Käsittelee API::V1::BooksController#index as */*
Kirjojen lataus (0.1ms) SELECT "books".* FROM "books" (kirjat)
↳ app/controllers/api/v1/books_controller.rb:7:in `index'
Kirjailijoiden lataus (0.1ms) SELECT "authors".* FROM "authors" WHERE "authors". "id" = ? LIMIT ? [[["id", 1], ["LIMIT", 1]]
↳ app/controllers/api/v1/books_controller.rb:7:in `index'
Kirjailijoiden lataus (0.1ms) SELECT "authors".* FROM "authors" WHERE "authors". "id" = ? LIMIT ? [[["id", 2], ["LIMIT", 1]]
↳ app/controllers/api/v1/books_controller.rb:7:in `index'
Kirjailijan lataus (0.1ms) SELECT "authors".* FROM "authors" WHERE "authors". "id" = ? LIMIT ? [["id", 3], ["LIMIT", 1]]
↳ app/controllers/api/v1/books_controller.rb:7:in `index'
Suoritettu 200 OK 6ms:ssa (Views: 0.1ms | ActiveRecord: 0.4ms | Allocations: 3134).
Käyttämällä assosiointia sarjallistajissamme otimme käyttöön n+1
ongelma. Haluamme poistaa sen lisäämällä käyttäjälle kontrollin siitä, mitä hän pyytää tässä päätepisteessä. Hänen pitäisi siis pystyä joko lataamaan vain kirjoja tai syöttämään includes-parametri ja saamaan myös kirjailijat, mutta mieluiten ilman includes-parametria. n+1
.
Määritellään vakio, joka pitää tietoa siitä, mitä kirjojen assokseja käyttäjä voi sisällyttää tiedostoon books#index
toiminta:
# lib/constants/books/includes.rb
moduuli Vakiot
moduuli Kirjat
moduuli Includes
ALLOWED = {
index: %i[
kirjoittaja
].freeze
}.freeze
end
end
end
Seuraavaksi määrittelemme nimiavaruuden tyhjille objektivakioille:
# lib/constants/empty.rb
moduuli Vakiot
moduuli Tyhjä
HASH = {}.freeze
end
end
Ja tässä on tärkein palvelumme lupien myöntämiseen sisältää. Luulen, että koodi on melko itsestään selvä, joitakin osia magic
jaetaan vain #default_resources_key (#default_resources_key)
ja #default_purpose
. Nämä metodit on määritelty, jotta voimme kutsua permit includes -metodeja välittämällä vain parametrit railsin ohjaimissa. Tuloksena on hash, joka tallentaa true
kunkin sallitun sisällyttämisen osalta.
# app/services/permit_includes.rb
require 'constants/empty'
require 'constants/books/includes'
luokka PermitIncludes
Empty = Constants::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 ellei includes_sent?(params)
return Empty::HASH ellei includes_valid?(params)
requested_includes = parse_includes(params)
allowed_includes = filter_includes(requested_includes, resources, purpose)
allowed_includes.index_with(true)
end
private
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
end
Nyt meidän on käytettävä avaimia sisällyttöjen lataamiseen ja välitettävä itse sisällyttöjen hash serializerille:
# app/controllers/api/v1/books_controller.rb
moduuli API
moduuli V1
class KirjatController < 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
Ja näin meidän on säädettävä sarjallistajamme - lataamme assosiaation vain, jos se on mukana:
# app/blueprints/book_blueprint.rb
class BookBlueprint (_field_name, _book, options) {
options[:includes] && options[:includes][:author]
}
end
Testataan sitä uudelleen:
kiskot s
curl http://localhost:3000/api/v1/books
# => [{"id":1, "title": "Kolme muskettisoturia"},{"id":2, "title": "Leijona, noita ja vaatekaappi"},{"id":3, "title": "Clean Code"}]
#-pyyntölokit (lataamme vain kirjoja)
Käynnistetty GET "/api/v1/books" for ::1 at 2021-12-24 10:33:41 +0100
Käsittelee API::V1::BooksController#index as */*
(0.1ms) SELECT sqlite_version(*)
↳ app/controllers/api/v1/books_controller.rb:8:in `index'
Kirjojen lataus (0.1ms) SELECT "books".* FROM "books"
↳ app/controllers/api/v1/books_controller.rb:8:in `index'
Suoritettu 200 OK 9ms:ssa (Views: 0.1ms | ActiveRecord: 0.9ms | Allocations: 4548)
Hyvä, me emme ole läpäisseet sisällyttämistä, joten saimme vain kirjoja, ilman kirjailijoita. Pyydetään nyt niitä:
curl 'http://localhost:3000/api/v1/books?includes=author'
# => [{"id":1, "author":{"id":1, "name": "Alexandre Dumas"}, "title": "Kolme muskettisoturia"},{"id":2, "author":{"id":2, "name": "C.S. Lewis"}, "nimi": "Leijona, noita ja vaatekaappi"},{"id":3, "kirjailija":{"id":3, "nimi": "Robert C. Martin"}, "nimi": "Clean Code"}]%
#-pyyntöjen lokit (poistettu n+1)
Käynnistetty GET "/api/v1/books?includes=author" for ::1 at 2021-12-24 10:38:23 +0100
Käsittelee API::V1::BooksController#index as */*
Parametrit: {"includes"=>"author"}
Kirjan lataus (0.1ms) SELECT "books".* FROM "books" (kirjat)
↳ app/controllers/api/v1/books_controller.rb:8:in `index'
Kirjailijoiden lataus (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 `indeksi'
Suoritettu 200 OK 17ms:ssa (Views: 0.1ms | ActiveRecord: 0.7ms | Allocations: 7373).
Siistiä! Saimme yhdistyksen ladattua ja eliminoitu n+1
ongelma. Palvelua voidaan käyttää mille tahansa resurssille, haluamme vain lisätä sallitut vakiot oikeassa muodossa ja lisätä ne osoitteeseen PermitIncludes::ALLOWED_INCLUDES
.
Meidän on muistettava, että tätä pitäisi luultavasti käyttää sivumäärittelyn (ja varovaisuuden) kanssa, koska assosiaatioiden sisällyttäminen voi "syödä" paljon muistia.