5 Beispiele für die beste Verwendung von Ruby
Haben Sie sich jemals gefragt, was wir mit Ruby alles machen können? Nun, der Himmel ist wahrscheinlich die Grenze, aber wir sprechen gerne über einige mehr oder weniger bekannte Fälle...
Wir werden eine Bücherregal-App entwickeln, die Bücher mit (oder ohne) Autorendaten auflistet.
Wir werden eine Bücherregal-App entwickeln, die Bücher mit (oder ohne) Autorendaten auflistet. Es wird eine einzelne #index
Aktion und einige Seeds. Dies ist eine Beispielanwendung, die zeigt, wie man einem Benutzer die Kontrolle über enthaltene Sub-Ressourcen in einer REST-ähnlichen API.
enthält
Abfrageparameter zum Laden zugehöriger Ressourcen (Autor
).enthält
Abfrageparameter hat das Format String: durch Kommata getrennte Wörter, die verschachtelte Ressourcen darstellen.Wir werden die Blaudrucker
als Serialisierer, weil er formatunabhängig und ziemlich flexibel ist. Dies ist ein einziges Juwel, das wir dem Standard-Toolset von Rails hinzufügen werden.
Lassen Sie uns eine Beispielanwendung erstellen. Wir fügen kein Test-Framework hinzu, da dies nicht in unseren Aufgabenbereich fällt.
Schienen neues Bücherregal -T
Jetzt erstellen Autor
Modell:
schienen g modell autor name:string
#=> Aufrufen von active_record
#=> db/migrate/20211224084524_create_authors.rb erstellen
#=> app/models/author.rb erstellen
Und Buchen Sie
:
schienen g model buch autor:referenzen titel:string
# => active_record aufrufen
# => db/migrate/20211224084614_create_books.rb erstellen
# => app/models/book.rb erstellen
Wir brauchen einige Samen:
# db/seeds.rb
dumas = Autor.erstellen(Name: 'Alexandre Dumas')
lewis = Autor.erstellen(Name: 'C.S. Lewis')
martin = Autor.erstellen(Name: 'Robert C. Martin')
Book.create(Autor: Dumas, Titel: 'Die drei Musketiere')
Book.create(author: lewis, title: 'Der Löwe, die Hexe und der Kleiderschrank')
Book.create(Autor: martin, Titel: 'Clean Code')
Jetzt sind wir bereit, Migrationen durchzuführen und die Datenbank zu starten:
schienen db:migrate && schienen db:seed
Fügen wir hinzu hat_viele
für Bücher in Autor
Modell:
# app/models/author.rb
Klasse Autor < ApplicationRecord
has_many :books
end
Es ist an der Zeit, einen Controller zu schreiben, der unsere Daten zurückgibt. Wir verwenden API
Namespace, also fügen wir zunächst ein Akronym für Beugungen hinzu:
# config/initializers/inflections.rb
ActiveSupport::Inflector.inflections(:en) do |inflect|
inflect.acronym 'API'
end
Ok, fügen wir unseren Serialisierer zu Gemfile
:
# Zum Gemfile hinzufügen
gem 'blueprinter'
Und natürlich installieren:
Bündelinstallation
Dann können wir unsere Entwürfe erstellen:
# app/blueprints/author_blueprint.rb
Klasse AuthorBlueprint < Blueprinter::Base
Bezeichner :id
Felder :name
end
# app/blueprints/book_blueprint.rb
Klasse BookBlueprint < Blueprinter::Base
Bezeichner :id
Felder :title
association :author, blueprint: AuthorBlueprint
end
Hinzufügen eines Basis-Controllers für API
:
# app/controllers/api/v1/base_controller.rb
Baustein API
Modul V1
Klasse BaseController < ActionController::API
end
end
end
Und die Entwurfsfassung unserer BooksController
:
# app/controllers/api/v1/books_controller.rb
Modul API
Modul V1
Klasse BooksController < BaseController
def index
books = Book.all
render json: BookBlueprint.render(books)
end
end
end
end
Natürlich müssen wir auch das Routing definieren:
# config/routes.rb
Rails.application.routes.draw do
namespace :api do
Namensraum :v1 do
ressourcen :bücher, nur: :index
end
end
end
Testen wir, was wir bisher gemacht haben:
Schienen s
http://localhost:3000/api/v1/books locken.
# => [{"id":1, "author":{"id":1, "name": "Alexandre Dumas"}, "title": "Die drei Musketiere"},{"id":2, "author":{"id":2, "name": "C.S. Lewis"}, "Titel": "Der Löwe, die Hexe und der Kleiderschrank"},{"id":3, "author":{"id":3, "name": "Robert C. Martin"}, "Titel": "Clean Code"}]
Die Daten scheinen in Ordnung zu sein, aber was ist mit den Protokollen?
# Anforderungsprotokolle (n+1)
Gestartet GET "/api/v1/books" für 127.0.0.1 um 2021-12-24 10:19:40 +0100
Verarbeitung durch API::V1::BooksController#index als */*
Buch laden (0.1ms) SELECT "bücher".* FROM "bücher"
↳ app/controllers/api/v1/books_controller.rb:7:in `index'
Autor laden (0.1ms) SELECT "autoren".* FROM "autoren" WHERE "autoren". "id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]
↳ app/controllers/api/v1/books_controller.rb:7:in `index'
Autor laden (0.1ms) SELECT "autoren".* FROM "autoren" WHERE "autoren". "id" = ? LIMIT ? [["id", 2], ["LIMIT", 1]]
↳ app/controllers/api/v1/books_controller.rb:7:in `index'
Autor laden (0.1ms) SELECT "autoren".* FROM "autoren" WHERE "autoren". "id" = ? LIMIT ? [["id", 3], ["LIMIT", 1]]
↳ app/controllers/api/v1/books_controller.rb:7:in `index'
Abgeschlossen 200 OK in 6ms (Views: 0.1ms | ActiveRecord: 0.4ms | Allocations: 3134)
Durch die Verwendung von Assoziation in unseren Serialisierern haben wir n+1
Problem. Wir wollen es beseitigen, indem wir dem Benutzer eine Kontrolle darüber geben, was er an diesem Endpunkt anfordert. Er sollte also in der Lage sein, entweder nur Bücher zu laden, oder den Includes-Parameter zu übergeben und auch Autoren zu erhalten, aber vorzugsweise ohne den n+1
.
Definieren wir eine Konstante, die Informationen darüber enthält, welche Assoziationen von Büchern der Benutzer in Bücher#index
Aktion:
# lib/constants/books/includes.rb
Modul Konstanten
Modul Bücher
modul Includes
ALLOWED = {
index: %i[
Autor
].freeze
}.freeze
end
end
end
Als nächstes definieren wir einen Namensraum für leere Objektkonstanten:
# lib/constants/empty.rb
Modul Konstanten
modul Leer
HASH = {}.freeze
end
end
Und hier ist unser Hauptdienst für die Genehmigung von Includes. Ich denke, der Code ist ziemlich selbsterklärend, einige Teile von Magie
werden nur zugewiesen in #default_resources_key
und #default_purpose
. Diese Methoden sind so definiert, dass wir permit includes aufrufen können, die nur Parameter in Rails-Controllern übergeben. Die Ausgabe wird der Hash sein, der Folgendes speichert wahr
für jede zulässige Aufnahme.
# app/services/permit_includes.rb
require 'Konstanten/Leerzeichen'
require 'Konstanten/Bücher/Einzelteile'
Klasse PermitIncludes
Leer = Konstanten::Leer
COMMA = ','
SLASH = '/'
INCLUDES_FORMAT = /A[a-z]+(,[a-z]+)*z/.freeze
ALLOWED_INCLUDES = {
Bücher: Konstanten::Bücher::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
privat
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
Jetzt müssen wir die Schlüssel verwenden, um Includes zu laden und den Hash der Includes selbst an den Serialisierer zu übergeben:
# app/controllers/api/v1/books_controller.rb
Modul API
Modul V1
Klasse 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
Und so müssen wir unseren Serializer anpassen - wir laden die Assoziation nur, wenn sie enthalten ist:
# app/blueprints/book_blueprint.rb
Klasse BookBlueprint (_field_name, _book, options) {
options[:includes] && options[:includes][:author]
}
end
Testen wir es noch einmal:
Schienen s
kräuseln http://localhost:3000/api/v1/books
# => [{"id":1, "title": "Die drei Musketiere"},{"id":2, "title": "Der Löwe, die Hexe und der Kleiderschrank"},{"id":3, "title": "Clean Code"}]
# Anfrageprotokolle (wir laden nur Bücher)
Startet GET "/api/v1/books" für ::1 am 2021-12-24 10:33:41 +0100
Verarbeitung durch API::V1::BooksController#index als */*
(0.1ms) SELECT sqlite_version(*)
↳ app/controllers/api/v1/books_controller.rb:8:in `index'
Buch laden (0.1ms) SELECT "bücher".* FROM "bücher"
↳ app/controllers/api/v1/books_controller.rb:8:in `index'
Abgeschlossen 200 OK in 9ms (Views: 0.1ms | ActiveRecord: 0.9ms | Allocations: 4548)
Gut, wir haben die Inklusion nicht bestanden, also haben wir nur Bücher, ohne Autoren. Fordern wir sie nun an:
curl 'http://localhost:3000/api/v1/books?includes=author'
# => [{"id":1, "author":{"id":1, "name": "Alexandre Dumas"}, "title": "Die drei Musketiere"},{"id":2, "author":{"id":2, "name": "C.S. Lewis"}, "Titel": "Der Löwe, die Hexe und der Kleiderschrank"},{"id":3, "author":{"id":3, "name": "Robert C. Martin"}, "Titel": "Clean Code"}]%
#-Anforderungsprotokolle (eliminiert n+1)
GET "/api/v1/books?includes=author" für ::1 gestartet am 2021-12-24 10:38:23 +0100
Verarbeitung durch API::V1::BooksController#index als */*
Parameter: {"includes"=>"author"}
Buch laden (0.1ms) SELECT "bücher".* FROM "bücher"
↳ app/controllers/api/v1/books_controller.rb:8:in `index'
Autor laden (0.2ms) SELECT "autoren".* FROM "autoren" WHERE "autoren". "id" IN (?, ?, ?) [["id", 1], ["id", 2], ["id", 3]]
↳ app/controllers/api/v1/books_controller.rb:8:in `index'
Abgeschlossen 200 OK in 17ms (Views: 0.1ms | ActiveRecord: 0.7ms | Allocations: 7373)
Super! Wir haben die Assoziation geladen und eliminiert n+1
Problem. Der Dienst kann für jede beliebige Ressource verwendet werden. Alles, was wir tun wollen, ist, zulässige Konstanten im richtigen Format hinzuzufügen und sie in PermitIncludes::ALLOWED_INCLUDES
.
Wir müssen bedenken, dass dies wahrscheinlich mit Paginierung (und Vorsicht) verwendet werden sollte, da die Einbeziehung von Assoziationen viel Speicher "fressen" kann.