Come Sequel e Sinatra risolvono il problema dell'API di Ruby
Pubblicato: 2022-03-11introduzione
Negli ultimi anni, il numero di framework di applicazioni JavaScript a pagina singola e di applicazioni mobili è aumentato notevolmente. Ciò impone un corrispondente aumento della domanda di API lato server. Poiché Ruby on Rails è uno dei framework di sviluppo web più popolari di oggi, è una scelta naturale tra molti sviluppatori per la creazione di applicazioni API back-end.
Tuttavia, mentre il paradigma architettonico di Ruby on Rails rende abbastanza facile creare applicazioni API di back-end, l'utilizzo di Rails solo per l'API è eccessivo. In effetti, è eccessivo al punto che anche il team di Rails lo ha riconosciuto e ha quindi introdotto una nuova modalità solo API nella versione 5. Con questa nuova funzionalità in Ruby on Rails, creare applicazioni solo API in Rails è diventato ancora più semplice e un'opzione più praticabile.
Ma ci sono anche altre opzioni. Le più notevoli sono due gemme molto mature e potenti, che in combinazione forniscono potenti strumenti per la creazione di API lato server. Sono Sinatra e Sequel.
Entrambe queste gemme hanno un set di funzionalità molto ricco: Sinatra funge da DSL (domain specific language) per le applicazioni Web e Sequel funge da livello di mappatura relazionale a oggetti (ORM). Quindi, diamo una breve occhiata a ciascuno di essi.
Sinatra
Sinatra è un framework per applicazioni web basato su Rack. The Rack è una ben nota interfaccia per server web Ruby. È utilizzato da molti framework, come Ruby on Rails, ad esempio, e supporta molti server Web, come WEBrick, Thin o Puma. Sinatra fornisce un'interfaccia minima per la scrittura di applicazioni Web in Ruby e una delle sue caratteristiche più interessanti è il supporto per i componenti middleware. Questi componenti si trovano tra l'applicazione e il server Web e possono monitorare e manipolare richieste e risposte.
Per utilizzare questa funzione Rack, Sinatra definisce DSL interno per la creazione di applicazioni web. La sua filosofia è molto semplice: i percorsi sono rappresentati da metodi HTTP, seguiti da un percorso che corrisponde a un modello. Un blocco Ruby all'interno del quale viene elaborata la richiesta e viene formata la risposta.
get '/' do 'Hello from sinatra' end
Il modello di corrispondenza del percorso può includere anche un parametro denominato. Quando viene eseguito il blocco di route, un valore di parametro viene passato al blocco tramite la variabile params
.
get '/players/:sport_id' do # Parameter value accessible through params[:sport_id] end
I modelli di corrispondenza possono utilizzare l'operatore splat *
che rende disponibili i valori dei parametri tramite params[:splat]
.
get '/players/*/:year' do # /players/performances/2016 # Parameters - params['splat'] -> ['performances'], params[:year] -> 2016 end
Questa non è la fine delle possibilità di Sinatra relative all'abbinamento dei percorsi. Può utilizzare una logica di corrispondenza più complessa tramite espressioni regolari, nonché corrispondenze personalizzate.
Sinatra comprende tutti i verbi HTTP standard necessari per la creazione di un'API REST: Get, Post, Put, Patch, Delete e Options. Le priorità del percorso sono determinate dall'ordine in cui sono definite e il primo percorso che corrisponde a una richiesta è quello che serve quella richiesta.
Le applicazioni Sinatra possono essere scritte in due modi; utilizzando lo stile classico o modulare. La principale differenza tra loro è che, con lo stile classico, possiamo avere una sola applicazione Sinatra per processo Ruby. Altre differenze sono abbastanza lievi da poter essere ignorate, nella maggior parte dei casi, e utilizzare le impostazioni predefinite.
Approccio classico
L'implementazione dell'applicazione classica è semplice. Non ci resta che caricare Sinatra e implementare i gestori di percorsi:
require 'sinatra' get '/' do 'Hello from Sinatra' end
Salvando questo codice nel file demo_api_classic.rb
, possiamo avviare l'applicazione direttamente eseguendo il seguente comando:
ruby demo_api_classic.rb
Tuttavia, se l'applicazione deve essere distribuita con gestori Rack, come Passenger, è meglio avviarla con il file config.ru
di configurazione Rack.
require './demo_api_classic' run Sinatra::Application
Con il file config.ru
in atto, l'applicazione viene avviata con il seguente comando:
rackup config.ru
Approccio modulare
Le applicazioni modulari Sinatra vengono create sottoclassi Sinatra::Base
o Sinatra::Application
:
require 'sinatra' class DemoApi < Sinatra::Application # Application code run! if app_file == $0 end
L'istruzione che inizia con run!
viene utilizzato per avviare l'applicazione direttamente, con ruby demo_api.rb
, proprio come con l'applicazione classica. D'altra parte, se l'applicazione deve essere distribuita con Rack, il contenuto dei gestori di rackup.ru
deve essere:
require './demo_api' run DemoApi
Continuazione
Il sequel è il secondo strumento di questo set. A differenza di ActiveRecord, che fa parte di Ruby on Rails, le dipendenze di Sequel sono molto piccole. Allo stesso tempo, è piuttosto ricco di funzionalità e può essere utilizzato per tutti i tipi di attività di manipolazione del database. Con il suo semplice linguaggio specifico del dominio, Sequel solleva lo sviluppatore da tutti i problemi con il mantenimento delle connessioni, la costruzione di query SQL, il recupero dei dati (e l'invio di dati al) database.
Ad esempio, stabilire una connessione con il database è molto semplice:
DB = Sequel.connect(adapter: :postgres, database: 'my_db', host: 'localhost', user: 'db_user')
Il metodo connect restituisce un oggetto database, in questo caso Sequel::Postgres::Database
, che può essere ulteriormente utilizzato per eseguire SQL grezzo.
DB['select count(*) from players']
In alternativa, per creare un nuovo oggetto dataset:
DB[:players]
Entrambe queste istruzioni creano un oggetto dataset, che è un'entità Sequel di base.
Una delle funzionalità più importanti del set di dati di Sequel è che non esegue query immediatamente. Ciò consente di archiviare set di dati per un uso successivo e, nella maggior parte dei casi, di concatenarli.
users = DB[:players].where(sport: 'tennis')
Quindi, se un set di dati non raggiunge immediatamente il database, la domanda è: quando lo fa? Sequel esegue SQL sul database quando vengono utilizzati i cosiddetti "metodi eseguibili". Questi metodi sono, per citarne alcuni, all
, each
, map
, first
e last
.
Il sequel è estensibile e la sua estensibilità è il risultato di una decisione architettonica fondamentale per costruire un piccolo core integrato con un sistema di plugin. Le funzionalità sono facilmente aggiunte tramite plugin che sono, in realtà, moduli Ruby. Il plugin più importante è il plugin Model
. È un plugin vuoto che non definisce da solo alcun metodo di classe o istanza. Al contrario, include altri plugin (sottomoduli) che definiscono una classe, un'istanza o metodi di set di dati del modello. Il plug-in Model consente l'uso di Sequel come strumento di mappatura relazionale degli oggetti (ORM) ed è spesso indicato come "plug-in di base".
class Player < Sequel::Model end
Il modello Sequel analizza automaticamente lo schema del database e imposta tutti i metodi di accesso necessari per tutte le colonne. Presuppone che il nome della tabella sia plurale e sia una versione sottolineata del nome del modello. Nel caso sia necessario lavorare con database che non seguono questa convenzione di denominazione, il nome della tabella può essere impostato in modo esplicito al momento della definizione del modello.
class Player < Sequel::Model(:player) end
Quindi, ora abbiamo tutto ciò di cui abbiamo bisogno per iniziare a creare l'API back-end.
Costruire l'API
Struttura del codice
Contrariamente a Rails, Sinatra non impone alcuna struttura progettuale. Tuttavia, poiché è sempre una buona pratica organizzare il codice per facilitare la manutenzione e lo sviluppo, lo faremo anche qui, con la seguente struttura di directory:

project root |-config |-helpers |-models |-routes
La configurazione dell'applicazione verrà caricata dal file di configurazione YAML per l'ambiente corrente con:
Sinatra::Application.config_file File.join(File.dirname(__FILE__), 'config', "#{Sinatra::Application.settings.environment}_config.yml")
Per impostazione predefinita, il valore di Sinatra::Applicationsettings.environment
è development,
e viene modificato impostando la variabile di ambiente RACK_ENV
.
Inoltre, la nostra applicazione deve caricare tutti i file dalle altre tre directory. Possiamo farlo facilmente eseguendo:
%w{helpers models routes}.each {|dir| Dir.glob("#{dir}/*.rb", &method(:require))}
A prima vista, questo modo di caricare potrebbe sembrare conveniente. Tuttavia, con questa riga di codice, non possiamo saltare facilmente i file, perché caricherà tutti i file dalle directory nell'array. Ecco perché useremo un approccio di caricamento di file singoli più efficiente, che presuppone che in ogni cartella abbiamo un file manifest init.rb
, che carica tutti gli altri file dalla directory. Inoltre, aggiungeremo una directory di destinazione al percorso di caricamento di Ruby:
%w{helpers models routes}.each do |dir| $LOAD_PATH << File.expand_path('.', File.join(File.dirname(__FILE__), dir)) require File.join(dir, 'init') end
Questo approccio richiede un po' più di lavoro perché dobbiamo mantenere le istruzioni require in ogni file init.rb
ma, in cambio, otteniamo un maggiore controllo e possiamo facilmente lasciare fuori uno o più file rimuovendoli dal file manifest init.rb
nella directory di destinazione.
Autenticazione API
La prima cosa di cui abbiamo bisogno in ogni API è l'autenticazione. Lo implementeremo come modulo di supporto. La logica di autenticazione completa sarà nel file helpers/authentication.rb
.
require 'multi_json' module Sinatra module Authentication def authenticate! client_id = request['client_id'] client_secret = request['client_secret'] # Authenticate client here halt 401, MultiJson.dump({message: "You are not authorized to access this resource"}) unless authenticated? end def current_client @current_client end def authenticated? !current_client.nil? end end helpers Authentication end
Tutto quello che dobbiamo fare ora è caricare questo file aggiungendo un'istruzione require nel file manifest dell'helper ( helpers/init.rb
) e chiamare l' authenticate!
metodo in before
hook di Sinatra che verrà eseguito prima di elaborare qualsiasi richiesta.
before do authenticate! end
Banca dati
Successivamente, dobbiamo preparare il nostro database per l'applicazione. Esistono molti modi per preparare il database, ma poiché stiamo usando Sequel, è naturale farlo utilizzando i migratori. Sequel viene fornito con due tipi di migratore: intero e basato su timestamp. Ognuno ha i suoi vantaggi e svantaggi. Nel nostro esempio, abbiamo deciso di utilizzare il migratore timestamp di Sequel, che richiede che i file di migrazione siano preceduti da un timestamp. Il migratore timestamp è molto flessibile e può accettare vari formati di timestamp, ma utilizzeremo solo quello composto da anno, mese, giorno, ora, minuto e secondo. Ecco i nostri due file di migrazione:
# db/migrations/20160710094000_sports.rb Sequel.migration do change do create_table(:sports) do primary_key :id String :name, :null => false end end end # db/migrations/20160710094100_players.rb Sequel.migration do change do create_table(:players) do primary_key :id String :name, :null => false foreign_key :sport_id, :sports end end end
Ora siamo pronti per creare un database con tutte le tabelle.
bundle exec sequel -m db/migrations sqlite://db/development.sqlite3
Infine, abbiamo i file modello sport.rb
e player.rb
nella directory models
.
# models/sport.rb class Sport < Sequel::Model one_to_many :players def to_api { id: id, name: name } end end # models/player.rb class Player < Sequel::Model many_to_one :sport def to_api { id: id, name: name, sport_id: sport_id } end end
Qui stiamo utilizzando un modo Sequel per definire le relazioni del modello, in cui l'oggetto Sport
ha molti giocatori e il Player
può avere un solo sport. Inoltre, ogni modello definisce il proprio metodo to_api
, che restituisce un hash con attributi che devono essere serializzati. Questo è un approccio generale che possiamo usare per vari formati. Tuttavia, se utilizzeremo solo un formato JSON nella nostra API, potremmo usare to_json
di Ruby con il only
argomento per limitare la serializzazione agli attributi richiesti, ad esempio player.to_json(only: [:id, :name, :sport_i])
. Naturalmente, potremmo anche definire un BaseModel
che eredita da Sequel::Model
e definisce un metodo to_api
predefinito, dal quale ereditare tutti i modelli potrebbero quindi ereditare.
Ora possiamo iniziare a implementare gli effettivi endpoint dell'API.
Endpoint API
Manterremo la definizione di tutti gli endpoint nei file all'interno della directory delle routes
. Poiché stiamo usando i file manifest per caricare i file, raggrupperemo i percorsi in base alle risorse (ad esempio, manterremo tutti i percorsi relativi allo sport nel file sports.rb
, tutti i percorsi dei giocatori in routes.rb
e così via).
# routes/sports.rb class DemoApi < Sinatra::Application get "/sports/?" do MultiJson.dump(Sport.all.map { |s| s.to_api }) end get "/sports/:id" do sport = Sport.where(id: params[:id]).first MultiJson.dump(sport ? sport.to_api : {}) end get "/sports/:id/players/?" do sport = Sport.where(id: params[:id]).first MultiJson.dump(sport ? sport.players.map { |p| p.to_api } : []) end end # routes/players.rb class DemoApi < Sinatra::Application get "/players/?" do MultiJson.dump(Player.all.map { |p| s.to_api }) end get "/players/:id/?" do player = Player.where(id: params[:id]).first MultiJson.dump(player ? player.to_api : {}) end end
I percorsi nidificati, come quello per portare tutti i giocatori all'interno di uno sport /sports/:id/players
, possono essere definiti posizionandoli insieme ad altri percorsi o creando un file di risorse separato che conterrà solo i percorsi nidificati.
Con i percorsi designati, l'applicazione è ora pronta per accettare le richieste:
curl -i -XGET 'http://localhost:9292/sports?client_id=<client_id>&client_secret=<client_secret>'
Si noti che, come richiesto dal sistema di autenticazione dell'applicazione definito nel file helpers/authentication.rb
, stiamo passando le credenziali direttamente nei parametri della richiesta.
Conclusione
I principi illustrati in questa semplice applicazione di esempio si applicano a qualsiasi applicazione di back-end API. Non si basa sull'architettura model-view-controller (MVC), ma mantiene una chiara separazione delle responsabilità in modo simile; la logica aziendale completa viene conservata nei file modello mentre la gestione delle richieste viene eseguita nei metodi di route di Sinatra. Contrariamente all'architettura MVC, in cui le viste vengono utilizzate per il rendering delle risposte, questa applicazione lo fa nello stesso punto in cui gestisce le richieste, nei metodi di route. Con i nuovi file helper, l'applicazione può essere facilmente estesa per inviare impaginazione o, se necessario, richiedere informazioni sui limiti all'utente nelle intestazioni delle risposte.
Alla fine, abbiamo creato un'API completa con un set di strumenti molto semplice e senza perdere alcuna funzionalità. Il numero limitato di dipendenze aiuta a garantire che l'applicazione venga caricata e avviata molto più velocemente e abbia un footprint di memoria molto inferiore rispetto a quello basato su Rails. Quindi, la prossima volta che inizi a lavorare su una nuova API in Ruby, considera l'utilizzo di Sinatra e Sequel poiché sono strumenti molto potenti per un tale caso d'uso.