5 przykładów najlepszego wykorzystania Rubiego
Czy kiedykolwiek zastanawiałeś się, co możemy zrobić z Ruby? Cóż, niebo jest prawdopodobnie granicą, ale chętnie porozmawiamy o kilku mniej lub bardziej znanych przypadkach...
Zbudujemy aplikację półki z książkami, aby wyświetlić listę książek z (lub bez) danych autorów.
Zbudujemy aplikację półki z książkami, aby wyświetlić listę książek z (lub bez) danych autorów. Dostępna będzie jedna aplikacja #index
i kilka nasion. Będzie to przykładowa aplikacja, która pokaże, w jaki sposób można dać użytkownikowi kontrolę nad dołączonymi plikami. pod-zasoby w interfejsie API typu REST.
zawiera
parametr zapytania w celu załadowania powiązanych zasobów (autor
).zawiera
ma format ciągu znaków: słowa oddzielone przecinkami, reprezentujące zagnieżdżone zasoby.Będziemy używać blueprinter
jako serializatora, ponieważ jest on niezależny od formatu i dość elastyczny. Jest to jedyny klejnot, który dodamy do standardowego zestawu narzędzi Rails.
Stwórzmy przykładową aplikację. Nie będziemy dodawać frameworka testowego, ponieważ jest on poza naszym zakresem.
szyny nowy regał -T
Teraz utwórz Autor
model:
rails g model author name:string
#=> wywołaj active_record
#=> create db/migrate/20211224084524_create_authors.rb
#=> create app/models/author.rb
I Książka
:
rails g model book author:references title:string
# => wywołaj active_record
# => create db/migrate/20211224084614_create_books.rb
# => create app/models/book.rb
Będziemy potrzebować nasion:
# 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(author: dumas, title: 'Trzej muszkieterowie')
Book.create(autor: lewis, tytuł: 'Lew, czarownica i stara szafa')
Book.create(author: martin, title: 'Czysty kod')
Teraz jesteśmy gotowi do uruchomienia migracji i zasiania bazy danych:
rails db:migrate && rails db:seed
Dodajmy has_many
dla książek w Autor
model:
# app/models/author.rb
class Author < ApplicationRecord
has_many :books
end
Nadszedł czas, aby napisać kontroler, który zwróci nasze dane. Użyjemy API
więc najpierw dodajmy akronim do fleksji:
# config/initializers/inflections.rb
ActiveSupport::Inflector.inflections(:en) do |inflect|
inflect.acronym 'API'
end
Ok, dodajmy nasz serializator do Gemfile
:
# Dodaj do Gemfile
gem 'blueprinter'
I oczywiście zainstalować:
instalacja pakietu
Następnie możemy zbudować nasze plany:
# app/blueprints/author_blueprint.rb
class AuthorBlueprint < Blueprinter::Base
identyfikator :id
fields :name
end
# app/blueprints/book_blueprint.rb
class BookBlueprint < Blueprinter::Base
identyfikator :id
fields :title
association :author, blueprint: AuthorBlueprint
end
Dodaj kontroler bazowy dla API
:
# app/controllers/api/v1/base_controller.rb
moduł API
moduł V1
class BaseController < ActionController::API
end
end
end
A wersja robocza naszego BooksController
:
# app/controllers/api/v1/books_controller.rb
moduł API
moduł V1
class BooksController < BaseController
def index
books = Book.all
render json: BookBlueprint.render(books)
end
end
end
end
Musimy też oczywiście zdefiniować routing:
# config/routes.rb
Rails.application.routes.draw do
namespace :api do
namespace :v1 do
resources :books, only: :index
end
end
end
Przetestujmy to, co zrobiliśmy do tej pory:
szyny s
curl http://localhost:3000/api/v1/books
# => [{"id":1, "author":{"id":1, "name": "Alexandre Dumas"}, "title": "Trzej muszkieterowie"},{"id":2, "author":{"id":2, "name": "C.S. Lewis"}, "title": "Lew, czarownica i stara szafa"},{"id":3, "author":{"id":3, "name": "Robert C. Martin"}, "title": "Clean Kod"}]
Dane wydają się być w porządku, a co z logami?
Rejestry żądań # (n+1)
Rozpoczęto GET "/api/v1/books" dla 127.0.0.1 o 2021-12-24 10:19:40 +0100
Przetwarzanie przez API::V1::BooksController#index jako */*
Ładowanie książki (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'
Ukończono 200 OK w 6 ms (Views: 0,1 ms | ActiveRecord: 0,4 ms | Allocations: 3134)
Używając asocjacji w naszych serializatorach wprowadziliśmy n+1
problem. Chcemy go wyeliminować, dodając użytkownikowi kontrolę nad tym, czego żąda w tym punkcie końcowym. Powinien więc być w stanie albo załadować tylko książki, albo przekazać parametr includes i pobrać również autorów, ale najlepiej bez parametru n+1
.
Zdefiniujmy stałą, która będzie przechowywać informacje o tym, jakie asocjacje książek użytkownik może umieścić w books#index
działanie:
# lib/constants/books/includes.rb
moduł Constants
moduł Books
moduł Includes
ALLOWED = {
index: %i[
autor
freeze
}.freeze
koniec
end
end
Następnie definiujemy przestrzeń nazw dla pustych stałych obiektu:
# lib/constants/empty.rb
moduł Constants
moduł Empty
HASH = {}.freeze
end
end
A oto nasza główna usługa zezwalająca na dołączanie. Myślę, że kod jest dość oczywisty, niektóre elementy magia
są przydzielane tylko w #default_resources_key
i #default_purpose
. Metody te są zdefiniowane, aby umożliwić nam wywoływanie zezwoleń obejmujących przekazywanie tylko parametrów w kontrolerach rails. Wynikiem będzie hash, który przechowuje prawda
dla każdego dozwolonego włączenia.
# app/services/permit_includes.rb
require 'constants/empty'
require 'constants/books/includes'
class 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)
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
Teraz musimy użyć kluczy do załadowania include'ów i przekazać sam hash do serializera:
# app/controllers/api/v1/books_controller.rb
moduł API
moduł V1
class BooksController < 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
I właśnie w ten sposób musimy dostosować nasz serializator - ładujemy asocjację tylko wtedy, gdy jest dołączona:
# app/blueprints/book_blueprint.rb
class BookBlueprint (_field_name, _book, options) {
options[:includes] && options[:includes][:author]
}
end
Przetestujmy to jeszcze raz:
szyny s
curl http://localhost:3000/api/v1/books
# => [{"id":1, "title": "Trzej muszkieterowie"},{"id":2, "title": "Lew, czarownica i stara szafa"},{"id":3, "title": "Czysty kod"}].
Dzienniki żądań # (ładujemy tylko książki)
Uruchomiono GET "/api/v1/books" dla ::1 o 2021-12-24 10:33:41 +0100
Przetwarzanie przez API::V1::BooksController#index jako */*
(0.1ms) SELECT sqlite_version(*)
↳ app/controllers/api/v1/books_controller.rb:8:in `index'
Ładowanie książki (0.1ms) SELECT "books".* FROM "books"
↳ app/controllers/api/v1/books_controller.rb:8:in `index'
Ukończono 200 OK w 9ms (Views: 0.1ms | ActiveRecord: 0.9ms | Allocations: 4548)
Dobrze, nie przeszliśmy przez include, więc mamy tylko książki, bez autorów. Zażądajmy ich teraz:
curl 'http://localhost:3000/api/v1/books?includes=author'
# => [{"id":1, "author":{"id":1, "name": "Alexandre Dumas"}, "title": "Trzej muszkieterowie"},{"id":2, "author":{"id":2, "name": "C.S. Lewis"}, "title": "Lew, czarownica i stara szafa"},{"id":3, "author":{"id":3, "name": "Robert C. Martin"}, "title": "Czysty kod"}]%
Dzienniki żądań # (wyeliminowane n+1)
Uruchomiono GET "/api/v1/books?includes=author" dla ::1 o 2021-12-24 10:38:23 +0100
Przetwarzanie przez API::V1::BooksController#index jako */*
Parametry: {"includes"=>"author"}
Ładowanie książki (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'
Ukończono 200 OK w 17 ms (Views: 0,1 ms | ActiveRecord: 0,7 ms | Allocations: 7373)
Super! Stowarzyszenie zostało załadowane i wyeliminowane n+1
problem. Usługa może być używana dla dowolnego zasobu, wszystko co chcemy zrobić, to dodać dozwolone stałe w odpowiednim formacie i dodać je do PermitIncludes::ALLOWED_INCLUDES
.
Musimy pamiętać, że powinno to być prawdopodobnie używane z paginacją (i ostrożnością), ponieważ włączenie skojarzeń może "zjeść" dużo pamięci.