Cum Sequel și Sinatra rezolvă problema API-ului lui Ruby
Publicat: 2022-03-11Introducere
În ultimii ani, numărul cadrelor de aplicații JavaScript cu o singură pagină și al aplicațiilor mobile a crescut substanțial. Acest lucru impune o cerere crescută în mod corespunzător pentru API-uri pe server. Întrucât Ruby on Rails este unul dintre cele mai populare cadre de dezvoltare web de astăzi, este o alegere naturală printre mulți dezvoltatori pentru crearea de aplicații API back-end.
Cu toate acestea, în timp ce paradigma arhitecturală Ruby on Rails face destul de ușoară crearea de aplicații API back-end, utilizarea Rails numai pentru API este exagerată. De fapt, este exagerat până în punctul în care chiar și echipa Rails a recunoscut acest lucru și, prin urmare, a introdus un nou mod numai API în versiunea 5. Cu această nouă caracteristică în Ruby on Rails, crearea de aplicații numai API în Rails a devenit și mai ușoară. și opțiune mai viabilă.
Dar există și alte opțiuni. Cele mai notabile sunt două pietre prețioase foarte mature și puternice, care în combinație oferă instrumente puternice pentru crearea de API-uri pe server. Ei sunt Sinatra și Sequel.
Ambele pietre prețioase au un set de caracteristici foarte bogat: Sinatra servește ca limbaj specific domeniului (DSL) pentru aplicațiile web, iar Sequel servește ca strat de cartografiere obiect-relațională (ORM). Deci, să aruncăm o scurtă privire asupra fiecăruia dintre ele.
Sinatra
Sinatra este un cadru de aplicații web bazat pe Rack. Rack-ul este o interfață de server web Ruby bine cunoscută. Este folosit de multe cadre, cum ar fi Ruby on Rails, de exemplu, și acceptă multe servere web, cum ar fi WEBrick, Thin sau Puma. Sinatra oferă o interfață minimă pentru scrierea aplicațiilor web în Ruby, iar una dintre caracteristicile sale cele mai convingătoare este suportul pentru componente middleware. Aceste componente se află între aplicație și serverul web și pot monitoriza și manipula cererile și răspunsurile.
Pentru utilizarea acestei caracteristici Rack, Sinatra definește DSL intern pentru crearea de aplicații web. Filosofia sa este foarte simplă: rutele sunt reprezentate prin metode HTTP, urmate de o rută care se potrivește cu un model. Un bloc Ruby în care se procesează cererea și se formează răspunsul.
get '/' do 'Hello from sinatra' end
Modelul de potrivire a rutei poate include, de asemenea, un parametru numit. Când blocarea rutei este executată, o valoare a parametrului este transmisă blocului prin variabila params
.
get '/players/:sport_id' do # Parameter value accessible through params[:sport_id] end
Modelele de potrivire pot folosi operatorul splat *
care face ca valorile parametrilor să fie disponibile prin params[:splat]
.
get '/players/*/:year' do # /players/performances/2016 # Parameters - params['splat'] -> ['performances'], params[:year] -> 2016 end
Acesta nu este sfârșitul posibilităților Sinatrei legate de potrivirea rutelor. Poate folosi o logică de potrivire mai complexă prin expresii regulate, precum și potriviri personalizate.
Sinatra înțelege toate verbele HTTP standard necesare pentru crearea unui API REST: Obține, Postează, Pune, Corectează, Șterge și Opțiuni. Prioritățile rutei sunt determinate de ordinea în care sunt definite, iar prima rută care se potrivește cu o cerere este cea care servește acea cerere.
Aplicațiile Sinatra pot fi scrise în două moduri; folosind stilul clasic sau modular. Principala diferență dintre ele este că, în stilul clasic, putem avea o singură aplicație Sinatra per proces Ruby. Alte diferențe sunt suficient de minore încât, în cele mai multe cazuri, pot fi ignorate, iar setările implicite pot fi utilizate.
Abordare clasică
Implementarea aplicației clasice este simplă. Trebuie doar să încărcăm Sinatra și să implementăm route handlere:
require 'sinatra' get '/' do 'Hello from Sinatra' end
Salvând acest cod în fișierul demo_api_classic.rb
, putem porni aplicația direct executând următoarea comandă:
ruby demo_api_classic.rb
Cu toate acestea, dacă aplicația urmează să fie implementată cu gestionare Rack, cum ar fi Passenger, este mai bine să o porniți cu fișierul de configurare Rack config.ru
.
require './demo_api_classic' run Sinatra::Application
Cu fișierul config.ru
la loc, aplicația este pornită cu următoarea comandă:
rackup config.ru
Abordare modulară
Aplicațiile modulare Sinatra sunt create prin subclasarea fie Sinatra::Base
fie Sinatra::Application
:
require 'sinatra' class DemoApi < Sinatra::Application # Application code run! if app_file == $0 end
Declarația care începe cu run!
este folosit pentru pornirea directă a aplicației, cu ruby demo_api.rb
, la fel ca și în cazul aplicației clasice. Pe de altă parte, dacă aplicația urmează să fie implementată cu Rack, conținutul de gestionare a rackup.ru
trebuie să fie:
require './demo_api' run DemoApi
Continuare
Sequel este al doilea instrument din acest set. Spre deosebire de ActiveRecord, care face parte din Ruby on Rails, dependențele Sequel sunt foarte mici. În același timp, este destul de bogat în caracteristici și poate fi folosit pentru toate tipurile de sarcini de manipulare a bazei de date. Cu limbajul său simplu specific domeniului, Sequel scutește dezvoltatorul de toate problemele legate de menținerea conexiunilor, construirea de interogări SQL, preluarea datelor din (și trimiterea datelor înapoi la) bază de date.
De exemplu, stabilirea unei conexiuni cu baza de date este foarte simplă:
DB = Sequel.connect(adapter: :postgres, database: 'my_db', host: 'localhost', user: 'db_user')
Metoda de conectare returnează un obiect de bază de date, în acest caz, Sequel::Postgres::Database
, care poate fi folosit în continuare pentru a executa SQL brut.
DB['select count(*) from players']
Alternativ, pentru a crea un nou obiect de set de date:
DB[:players]
Ambele declarații creează un obiect de set de date, care este o entitate Sequel de bază.
Una dintre cele mai importante caracteristici ale setului de date Sequel este că nu execută interogări imediat. Acest lucru face posibilă stocarea seturilor de date pentru utilizare ulterioară și, în cele mai multe cazuri, înlănțuirea acestora.
users = DB[:players].where(sport: 'tennis')
Deci, dacă un set de date nu ajunge imediat în baza de date, întrebarea este, când apare? Sequel execută SQL în baza de date atunci când sunt folosite așa-numitele „metode executabile”. Aceste metode sunt, pentru a numi câteva, all
, each
, map
, first
și last
.
Sequel este extensibil, iar extensibilitatea sa este rezultatul unei decizii arhitecturale fundamentale de a construi un nucleu mic completat cu un sistem de pluginuri. Caracteristicile sunt adăugate cu ușurință prin pluginuri care sunt, de fapt, module Ruby. Cel mai important plugin este pluginul Model
. Este un plugin gol care nu definește nicio clasă sau metode de instanță în sine. În schimb, include alte pluginuri (submodule) care definesc o clasă, o instanță sau o metodă de set de date model. Pluginul Model permite utilizarea Sequel ca instrument de cartografiere relațională a obiectelor (ORM) și este adesea denumit „plugin de bază”.
class Player < Sequel::Model end
Modelul Sequel analizează automat schema bazei de date și stabilește toate metodele accesorii necesare pentru toate coloanele. Se presupune că numele tabelului este la plural și este o versiune subliniată a numelui modelului. În cazul în care este nevoie de a lucra cu baze de date care nu respectă această convenție de denumire, numele tabelului poate fi setat în mod explicit atunci când modelul este definit.
class Player < Sequel::Model(:player) end
Deci, acum avem tot ce ne trebuie pentru a începe construirea API-ului back-end.
Construirea API-ului
Structura codului
Spre deosebire de Rails, Sinatra nu impune nicio structură de proiect. Cu toate acestea, deoarece este întotdeauna o practică bună să organizați codul pentru o întreținere și o dezvoltare mai ușoare, o vom face și aici, cu următoarea structură de directoare:

project root |-config |-helpers |-models |-routes
Configurația aplicației va fi încărcată din fișierul de configurare YAML pentru mediul curent cu:
Sinatra::Application.config_file File.join(File.dirname(__FILE__), 'config', "#{Sinatra::Application.settings.environment}_config.yml")
În mod implicit, valoarea Sinatra::Applicationsettings.environment
este development,
și este modificată prin setarea variabilei de mediu RACK_ENV
.
În plus, aplicația noastră trebuie să încarce toate fișierele din celelalte trei directoare. Putem face asta cu ușurință rulând:
%w{helpers models routes}.each {|dir| Dir.glob("#{dir}/*.rb", &method(:require))}
La prima vedere, acest mod de încărcare ar putea părea convenabil. Cu toate acestea, cu această singură linie a codului, nu putem sări peste fișiere cu ușurință, deoarece va încărca toate fișierele din directoarele din matrice. De aceea vom folosi o abordare mai eficientă a încărcării unui singur fișier, care presupune că în fiecare folder avem un fișier manifest init.rb
, care încarcă toate celelalte fișiere din director. De asemenea, vom adăuga un director țintă la calea de încărcare 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
Această abordare necesită ceva mai multă muncă, deoarece trebuie să menținem instrucțiunile require în fiecare fișier init.rb
, dar, în schimb, obținem mai mult control și putem lăsa cu ușurință unul sau mai multe fișiere, eliminându-le din fișierul manifest init.rb
în directorul țintă.
Autentificare API
Primul lucru de care avem nevoie în fiecare API este autentificarea. Îl vom implementa ca modul de ajutor. Logica de autentificare completă va fi în fișierul 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
Tot ce trebuie să facem acum este să încărcăm acest fișier adăugând o declarație require în fișierul manifest helper ( helpers/init.rb
) și să apelăm la authenticate!
metoda din sinatra before
hook care va fi executată înainte de procesarea oricărei cereri.
before do authenticate! end
Bază de date
În continuare, trebuie să ne pregătim baza de date pentru aplicație. Există multe modalități de a pregăti baza de date, dar din moment ce folosim Sequel, este firesc să o facem folosind migratori. Sequel vine cu două tipuri de migrator - bazat pe numere întregi și marcate de timp. Fiecare are avantajele și dezavantajele sale. În exemplul nostru, am decis să folosim migratorul de marcaj de timp al Sequel, care necesită ca fișierele de migrare să fie prefixate cu un marcaj de timp. Migratorul de marcaj de timp este foarte flexibil și poate accepta diverse formate de marcaj de timp, dar îl vom folosi doar pe cel care constă în an, lună, zi, oră, minut și secundă. Iată cele două fișiere de migrare ale noastre:
# 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
Acum suntem gata să creăm o bază de date cu toate tabelele.
bundle exec sequel -m db/migrations sqlite://db/development.sqlite3
În cele din urmă, avem fișierele model sport.rb
și player.rb
în directorul 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
Aici folosim un mod Sequel de a defini relațiile model, în care obiectul Sport
are mulți jucători și Player
poate avea un singur sport. De asemenea, fiecare model își definește metoda to_api
, care returnează un hash cu atribute care trebuie serializate. Aceasta este o abordare generală pe care o putem folosi pentru diferite formate. Cu toate acestea, dacă vom folosi doar un format JSON în API-ul nostru, am putea folosi to_json
al lui Ruby cu only
argument pentru a restricționa serializarea la atributele necesare, adică player.to_json(only: [:id, :name, :sport_i])
. Desigur, am putea defini și un BaseModel
care moștenește de la Sequel::Model
și definește o metodă implicită to_api
, de la care moștenesc toate modelele ar putea apoi moșteni.
Acum, putem începe să implementăm punctele finale API reale.
Puncte finale API
Vom păstra definiția tuturor punctelor finale în fișierele din directorul routes
. Deoarece folosim fișiere manifest pentru încărcarea fișierelor, vom grupa rutele după resurse (adică, păstrăm toate rutele legate de sport în fișierul sports.rb
, toate rutele jucătorilor în routes.rb
și așa mai departe).
# 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
Rutele imbricate, cum ar fi cea pentru a aduna toți jucătorii într-un sport /sports/:id/players
, pot fi fie definite prin plasarea lor împreună cu alte rute, fie prin crearea unui fișier de resurse separat care va conține doar rutele imbricate.
Cu rutele desemnate, aplicația este acum pregătită să accepte cereri:
curl -i -XGET 'http://localhost:9292/sports?client_id=<client_id>&client_secret=<client_secret>'
Rețineți că, așa cum este cerut de sistemul de autentificare al aplicației definit în fișierul helpers/authentication.rb
, transmitem acreditările direct în parametrii de solicitare.
Concluzie
Principiile demonstrate în acest exemplu simplu de aplicație se aplică oricărei aplicații back-end API. Nu se bazează pe arhitectura model-view-controller (MVC), dar păstrează o separare clară a responsabilităților într-un mod similar; logica de afaceri completă este păstrată în fișierele model în timp ce gestionarea cererilor se face în metodele de rute Sinatra. Spre deosebire de arhitectura MVC, în care vizualizările sunt folosite pentru a reda răspunsurile, această aplicație face asta în același loc în care se ocupă de cereri - în metodele rute. Cu noile fișiere de ajutor, aplicația poate fi extinsă cu ușurință pentru a trimite paginarea sau, dacă este necesar, pentru a solicita informații despre limite înapoi către utilizator în anteturile de răspuns.
În cele din urmă, am construit un API complet cu un set de instrumente foarte simplu și fără a pierde nicio funcționalitate. Numărul limitat de dependențe ajută la asigurarea faptului că aplicația se încarcă și pornește mult mai rapid și are o amprentă de memorie mult mai mică decât ar avea una bazată pe Rails. Deci, data viitoare când începeți să lucrați la un nou API în Ruby, luați în considerare utilizarea Sinatra și Sequel, deoarece sunt instrumente foarte puternice pentru un astfel de caz de utilizare.