5 příkladů nejlepšího použití jazyka Ruby
Přemýšleli jste někdy o tom, co všechno můžeme dělat s Ruby? No, obloze se asi meze nekladou, ale rádi si povíme o některých více či méně známých případech...
Vytvoříme aplikaci pro seznam knih s údaji o autorech (nebo bez nich).
Vytvoříme aplikaci pro seznam knih s autory (nebo bez nich). data. Bude existovat jediná #index akce a některá semena. Toto bude příklad aplikace, která ukáže, jak můžete dát uživateli kontrolu nad obsaženými dílčích zdrojů ve formátu REST. API.
obsahuje parametr dotazu pro načtení přidružených zdrojů (autor).obsahuje parametr dotazu má formát řetězce: slova oddělená čárkou, která představují vnořené zdroje.Použijeme blueprinter jako serializér, protože je formátově agnostický a poměrně flexibilní. Jedná se o jediný klenot, který přidáme do standardní sady nástrojů rails.
Vytvořme příklad aplikace. Nepřidáváme testovací framework, protože je mimo náš rozsah.
kolejnice nový regál -T
Nyní vytvořte Autor model:
rails g jméno autora modelu:string
#=> invoke active_record
#=> create db/migrate/20211224084524_create_authors.rb
#=> create app/models/author.rb
A Kniha:
rails g model book autor:reference název:string
# => invoke active_record
# => create db/migrate/20211224084614_create_books.rb
# => create app/models/book.rb
Budeme potřebovat semena:
# db/seeds.rb
dumas = Author.create(name: 'Alexandre Dumas')
lewis = Author.create(name: 'C.S. Lewis')
martin = Author.create(name: 'Robert C. Martin')
Book.create(autor: dumas, název: "Tři mušketýři")
Book.create(autor: lewis, název: "The Lion, the Witch and the Wardrobe")
Book.create(autor: martin, název: "Clean Code")
Nyní jsme připraveni spustit migraci a nasadit db:
rails db:migrate && rails db:seed
Přidejme has_many pro knihy v Autor model:
# app/models/author.rb
class Author < ApplicationRecord
has_many :books
end
Je čas napsat kontrolér, který bude vracet naše data. Použijeme API nejprve přidáme zkratku ke skloňování:
# config/initializers/inflections.rb
ActiveSupport::Inflector.inflections(:en) do |inflect|
inflect.acronym 'API'
end
Dobře, přidejme náš serializér do příkazu Gemfile:
# Přidat do souboru drahokamů
gem 'blueprinter'
A samozřejmě ji nainstalujte:
instalace svazku
Pak můžeme vytvořit naše plány:
# app/blueprints/author_blueprint.rb
class AuthorBlueprint < Blueprinter::Base
identifier :id
fields :name
end
# app/blueprints/book_blueprint.rb
class BookBlueprint < Blueprinter::Base
identifier :id
fields :title
asociace :author, blueprint: AuthorBlueprint
end
Přidání základního řadiče pro API:
# app/controllers/api/v1/base_controller.rb
modul API
modul V1
třída BaseController < ActionController::API
end
end
end
A návrh naší BooksController:
# app/controllers/api/v1/books_controller.rb
modul API
modul V1
třída BooksController < BaseController
def index
books = Book.all
render json: BookBlueprint.render(books)
end
end
end
end
Samozřejmě musíme také definovat směrování:
# config/routes.rb
Rails.application.routes.draw do
namespace :api do
namespace :v1 do
resources :books, only: :index
end
end
end
Vyzkoušejme, co jsme dosud udělali:
kolejnice s
curl http://localhost:3000/api/v1/books
# => [{"id":1, "author":{"id":1, "name": "Alexandre Dumas"}, "title": "The Three Musketeers"},{"id":2, "author":{"id":2, "name": "C.S. Lewis"}, "title": "The Lion, the Witch and the Wardrobe"},{"id":3, "author":{"id":3, "name": "Robert C. Martin"}, "title": "Clean Kód"}]
Data se zdají být v pořádku, ale co protokoly?
Protokoly požadavků # (n+1)
Spuštěno GET "/api/v1/books" pro 127.0.0.1 v 2021-12-24 10:19:40 +0100
Zpracování pomocí API::V1::BooksController#index jako */*
Načítání knih (0.1ms) 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'
Dokončeno 200 OK za 6 ms (Views: 0.1ms | ActiveRecord: 0.4ms | Allocations: 3134)
Použitím asociace v našich serializátorech jsme zavedli n+1 problém. Chceme ho odstranit tím, že přidáme uživateli kontrolu nad tím, co v tomto koncovém bodě požaduje. Měl by tedy mít možnost buď načíst pouze knihy, nebo předat parametr includes a získat i autory, ale nejlépe bez parametru n+1.
Definujme konstantu, která bude uchovávat informaci o tom, jaké asociace knih může uživatel zahrnout do. books#index akce:
# lib/constants/books/includes.rb
modul Constants
modul Books
modul Includes
ALLOWED = {
index: %i[
autor
].freeze
}.freeze
konec
konec
end
Dále definujeme jmenný prostor pro prázdné konstanty objektů:
# lib/constants/empty.rb
modul Constants
modul Empty
HASH = {}.freeze
konec
konec
A zde je naše hlavní služba pro povolení zahrnuje. Myslím, že kód je celkem srozumitelný, některé části kouzlo jsou přidělovány pouze v #default_resources_key a #default_purpose. Tyto metody jsou definovány tak, aby umožňovaly nás volání permit zahrnuje předávání pouze paramů v řadičích rails. Výstupem bude hash, který ukládá Pravda pro každé povolené zařazení.
# app/services/permit_includes.rb
require 'constants/empty'
require 'constants/books/includes'
třída 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 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)
konec
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
Nyní musíme použít klíče k načtení include a předat samotný hash inlcudes serializátoru:
# app/controllers/api/v1/books_controller.rb
modul API
modul V1
třída BooksController < BaseController
def index
includes = PermitIncludes.new.call(params)
books = Book.includes(includes.keys).all
vykreslit json: BookBlueprint.render(books, includes: includes)
konec
end
end
end
A právě tak musíme upravit náš serializér - asociaci načteme pouze tehdy, je-li obsažena:
# app/blueprints/book_blueprint.rb
class BookBlueprint (_field_name, _book, options) {
options[:includes] && options[:includes][:author]
}
end
Vyzkoušejme to znovu:
kolejnice s
curl 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"}]
Protokoly požadavků # (načítáme pouze knihy)
Spuštěno GET "/api/v1/books" pro ::1 v 2021-12-24 10:33:41 +0100
Zpracování pomocí API::V1::BooksController#index jako */*
(0.1ms) SELECT sqlite_version(*)
↳ app/controllers/api/v1/books_controller.rb:8:in `index'
Načítání knih (0.1ms) SELECT "books".* FROM "books"
↳ app/controllers/api/v1/books_controller.rb:8:in `dex'
Dokončeno 200 OK za 9 ms (Zobrazení: 0.1ms | ActiveRecord: 0.9ms | Alokace: 4548)
Dobře, neprošli jsme, takže jsme dostali jen knihy, bez autorů. Nyní si je vyžádáme:
curl 'http://localhost:3000/api/v1/books?includes=author'
# => [{"id":1, "author":{"id":1, "name": "Alexandre Dumas"}, "title": "The Three Musketeers"},{"id":2, "author":{"id":2, "name": "C.S. Lewis"}, "title": "The Lion, the Witch and the Wardrobe"},{"id":3, "author":{"id":3, "name": "Robert C. Martin"}, "title": "Clean Code"}]%
Protokoly požadavků # (vyřazeno n+1)
Spuštěno GET "/api/v1/books?includes=author" pro ::1 v 2021-12-24 10:38:23 +0100
Zpracování pomocí API::V1::BooksController#index jako */*
Parametry: {"includes"=>"author"}
Načítání knih (0.1ms) 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'
Dokončeno 200 OK za 17 ms (Zobrazení: 0,1 ms | ActiveRecord: 0,7 ms | Alokace: 7373)
Super! Sdružení jsme načetli a vyřadili n+1 problém. Služba může být použita pro jakýkoli zdroj, jediné, co chceme udělat, je přidat povolené konstanty ve správném formátu a přidat je do služby PermitIncludes::ALLOWED_INCLUDES.
Musíme si uvědomit, že by se to mělo pravděpodobně používat se stránkováním (a opatrně), protože zahrnutí asociací může "sežrat" hodně paměti.