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 údaji o autorech (nebo bez nich). Bude existovat jedna #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čí zdroje v rozhraní API typu REST.
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
end
end
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.