Jak sequel i Sinatra rozwiązują problem z interfejsem API Rubiego?

Opublikowany: 2022-03-11

Wstęp

W ostatnich latach znacznie wzrosła liczba jednostronicowych frameworków aplikacji JavaScript i aplikacji mobilnych. Narzuca to odpowiednio zwiększone zapotrzebowanie na interfejsy API po stronie serwera. Ponieważ Ruby on Rails jest jednym z najpopularniejszych obecnie frameworków do tworzenia stron internetowych, jest to naturalny wybór wśród wielu programistów do tworzenia aplikacji back-end API.

Jednak podczas gdy paradygmat architektury Ruby on Rails sprawia, że ​​tworzenie aplikacji back-end API jest dość łatwe, używanie Rails tylko dla API jest przesadą. W rzeczywistości jest to przesada do tego stopnia, że ​​nawet zespół Rails zauważył to i dlatego wprowadził nowy tryb API-only w wersji 5. Dzięki tej nowej funkcji w Ruby on Rails, tworzenie aplikacji tylko API w Rails stało się jeszcze łatwiejsze. i bardziej opłacalna opcja.

Ale są też inne opcje. Najbardziej godne uwagi są dwa bardzo dojrzałe i potężne klejnoty, które w połączeniu zapewniają potężne narzędzia do tworzenia API po stronie serwera. Są to Sinatra i Sequel.

Oba te klejnoty mają bardzo bogaty zestaw funkcji: Sinatra służy jako język specyficzny dla domeny (DSL) dla aplikacji internetowych, a Sequel służy jako warstwa mapowania obiektowo-relacyjnego (ORM). Przyjrzyjmy się więc pokrótce każdemu z nich.

API z Sinatrą i sequelem: samouczek Ruby

Ruby API na diecie: wprowadzenie do Sequela i Sinatry.
Ćwierkać

Synatra

Sinatra to platforma aplikacji internetowych oparta na stojaku. Rack jest dobrze znanym interfejsem serwera WWW Ruby. Jest używany przez wiele frameworków, na przykład Ruby on Rails, i obsługuje wiele serwerów internetowych, takich jak WEBrick, Thin czy Puma. Sinatra zapewnia minimalny interfejs do pisania aplikacji internetowych w Ruby, a jedną z jego najbardziej przekonujących funkcji jest obsługa komponentów oprogramowania pośredniego. Te składniki znajdują się między aplikacją a serwerem WWW i mogą monitorować żądania i odpowiedzi oraz manipulować nimi.

Aby wykorzystać tę funkcję Rack, Sinatra definiuje wewnętrzne łącze DSL do tworzenia aplikacji internetowych. Jego filozofia jest bardzo prosta: trasy są reprezentowane przez metody HTTP, po których następuje trasa pasująca do wzorca. Blok Rubiego, w ramach którego przetwarzane jest żądanie i tworzona jest odpowiedź.

 get '/' do 'Hello from sinatra' end

Wzorzec dopasowania trasy może również zawierać nazwany parametr. Kiedy wykonywany jest blok trasy, wartość parametru jest przekazywana do bloku przez zmienną params .

 get '/players/:sport_id' do # Parameter value accessible through params[:sport_id] end

Dopasowane wzorce mogą używać operatora splat * , który udostępnia wartości parametrów za pośrednictwem params[:splat] .

 get '/players/*/:year' do # /players/performances/2016 # Parameters - params['splat'] -> ['performances'], params[:year] -> 2016 end

To nie koniec możliwości Sinatry związanych z dopasowaniem tras. Może używać bardziej złożonej logiki dopasowywania za pomocą wyrażeń regularnych, a także niestandardowych dopasowań.

Sinatra rozumie wszystkie standardowe czasowniki HTTP potrzebne do stworzenia interfejsu API REST: Get, Post, Put, Patch, Delete i Options. Priorytety tras są określane na podstawie kolejności, w jakiej są zdefiniowane, a pierwsza trasa, która pasuje do żądania, to ta, która obsługuje to żądanie.

Aplikacje Sinatra można pisać na dwa sposoby; przy użyciu stylu klasycznego lub modułowego. Główna różnica między nimi polega na tym, że przy klasycznym stylu możemy mieć tylko jedną aplikację Sinatra na proces Ruby. Inne różnice są na tyle niewielkie, że w większości przypadków można je zignorować i można użyć ustawień domyślnych.

Podejście klasyczne

Wdrożenie klasycznej aplikacji jest proste. Musimy tylko załadować Sinatrę i wdrożyć obsługę tras:

 require 'sinatra' get '/' do 'Hello from Sinatra' end

Zapisując ten kod do pliku demo_api_classic.rb , możemy uruchomić aplikację bezpośrednio, wykonując następujące polecenie:

 ruby demo_api_classic.rb

Jeśli jednak aplikacja ma zostać wdrożona z modułami obsługi racka, takimi jak Passenger, lepiej jest uruchomić ją z plikiem konfiguracyjnym config.ru .

 require './demo_api_classic' run Sinatra::Application

Po zainstalowaniu pliku config.ru aplikacja jest uruchamiana za pomocą następującego polecenia:

 rackup config.ru

Podejście modułowe

Modułowe aplikacje Sinatra są tworzone przez tworzenie podklas Sinatra::Base lub Sinatra::Application :

 require 'sinatra' class DemoApi < Sinatra::Application # Application code run! if app_file == $0 end

Stwierdzenie zaczynające się od run! służy do bezpośredniego uruchamiania aplikacji za pomocą ruby demo_api.rb , tak jak w przypadku klasycznej aplikacji. Z drugiej strony, jeśli aplikacja ma zostać wdrożona z Rack, zawartość handlerów w rackup.ru musi być:

 require './demo_api' run DemoApi

Dalszy ciąg

Sequel to drugie narzędzie w tym zestawie. W przeciwieństwie do ActiveRecord, który jest częścią Ruby on Rails, zależności Sequela są bardzo małe. Jednocześnie jest dość bogaty w funkcje i może być używany do wszelkiego rodzaju zadań związanych z manipulacją bazą danych. Dzięki prostemu językowi specyficznemu dla domeny, Sequel uwalnia programistę od wszelkich problemów związanych z utrzymywaniem połączeń, konstruowaniem zapytań SQL, pobieraniem danych z (i wysyłaniem danych z powrotem) do bazy danych.

Na przykład nawiązanie połączenia z bazą danych jest bardzo proste:

 DB = Sequel.connect(adapter: :postgres, database: 'my_db', host: 'localhost', user: 'db_user')

Metoda connect zwraca obiekt bazy danych, w tym przypadku Sequel::Postgres::Database , który może być dalej wykorzystany do wykonania surowego SQL.

 DB['select count(*) from players']

Alternatywnie, aby utworzyć nowy obiekt zestawu danych:

 DB[:players]

Obie te instrukcje tworzą obiekt zestawu danych, który jest podstawową jednostką Sequel.

Jedną z najważniejszych cech zestawu danych Sequel jest to, że nie wykonuje on zapytań natychmiast. Umożliwia to przechowywanie zbiorów danych do późniejszego wykorzystania oraz, w większości przypadków, łączenie ich w łańcuch.

 users = DB[:players].where(sport: 'tennis')

Tak więc, jeśli zestaw danych nie trafia natychmiast do bazy danych, pytanie brzmi: kiedy to robi? Sequel wykonuje SQL na bazie danych, gdy używane są tak zwane „metody wykonywalne”. Te metody to między innymi all , each , map , first i last .

Sequel jest rozszerzalny, a jego rozszerzalność jest wynikiem fundamentalnej decyzji architektonicznej o zbudowaniu małego rdzenia uzupełnionego o system wtyczek. Funkcje można łatwo dodawać za pomocą wtyczek, które w rzeczywistości są modułami Ruby. Najważniejszą wtyczką jest wtyczka Model . Jest to pusta wtyczka, która sama nie definiuje żadnej klasy ani metody instancji. Zamiast tego zawiera inne wtyczki (podmoduły), które definiują klasę, instancję lub metody zestawu danych modelu. Wtyczka Model umożliwia korzystanie z Sequela jako narzędzia do mapowania obiektowo-relacyjnego (ORM) i jest często określana jako „wtyczka podstawowa”.

 class Player < Sequel::Model end

Model Sequel automatycznie analizuje schemat bazy danych i konfiguruje wszystkie niezbędne metody dostępu dla wszystkich kolumn. Zakłada, że ​​nazwa tabeli jest w liczbie mnogiej i jest podkreśloną wersją nazwy modelu. W przypadku konieczności pracy z bazami danych, które nie są zgodne z tą konwencją nazewnictwa, nazwę tabeli można ustawić jawnie podczas definiowania modelu.

 class Player < Sequel::Model(:player) end

Tak więc mamy teraz wszystko, czego potrzebujemy, aby rozpocząć tworzenie interfejsu API zaplecza.

Budowanie API

Struktura kodu

W przeciwieństwie do Rails, Sinatra nie narzuca żadnej struktury projektu. Ponieważ jednak zawsze dobrą praktyką jest organizowanie kodu w celu łatwiejszego utrzymania i rozwoju, zrobimy to również tutaj, z następującą strukturą katalogów:

 project root |-config |-helpers |-models |-routes

Konfiguracja aplikacji zostanie załadowana z pliku konfiguracyjnego YAML dla bieżącego środowiska z:

 Sinatra::Application.config_file File.join(File.dirname(__FILE__), 'config', "#{Sinatra::Application.settings.environment}_config.yml")

Domyślnie wartość Sinatra::Applicationsettings.environment to development, i jest zmieniana przez ustawienie zmiennej środowiskowej RACK_ENV .

Ponadto nasza aplikacja musi załadować wszystkie pliki z pozostałych trzech katalogów. Możemy to łatwo zrobić, uruchamiając:

 %w{helpers models routes}.each {|dir| Dir.glob("#{dir}/*.rb", &method(:require))}

Na pierwszy rzut oka taki sposób ładowania może wydawać się wygodny. Jednak przy tym jednym wierszu kodu nie możemy łatwo pominąć plików, ponieważ załaduje on wszystkie pliki z katalogów w tablicy. Dlatego zastosujemy wydajniejsze podejście do ładowania pojedynczego pliku, które zakłada, że ​​w każdym folderze mamy plik manifestu init.rb , który ładuje wszystkie inne pliki z katalogu. Ponadto dodamy katalog docelowy do ścieżki ładowania Rubiego:

 %w{helpers models routes}.each do |dir| $LOAD_PATH << File.expand_path('.', File.join(File.dirname(__FILE__), dir)) require File.join(dir, 'init') end

Takie podejście wymaga nieco więcej pracy, ponieważ musimy utrzymywać instrukcje wymagane w każdym pliku init.rb , ale w zamian uzyskujemy większą kontrolę i możemy łatwo pominąć jeden lub więcej plików, usuwając je z pliku manifestu init.rb w katalogu docelowym.

Uwierzytelnianie API

Pierwszą rzeczą, jakiej potrzebujemy w każdym API, jest uwierzytelnianie. Zaimplementujemy go jako moduł pomocniczy. Pełna logika uwierzytelniania będzie znajdować się w pliku 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

Wszystko, co musimy teraz zrobić, to załadować ten plik, dodając oświadczenie require w pliku manifestu pomocnika ( helpers/init.rb ) i wywołać authenticate! metoda w Sinatrze before przechwyceniem, która zostanie wykonana przed przetworzeniem jakiegokolwiek żądania.

 before do authenticate! end

Baza danych

Następnie musimy przygotować naszą bazę danych dla aplikacji. Istnieje wiele sposobów na przygotowanie bazy danych, ale ponieważ używamy Sequela, naturalne jest robienie tego za pomocą migratorów. Sequel jest dostarczany z dwoma typami migratorów - opartymi na liczbach całkowitych i znacznikach czasu. Każdy ma swoje zalety i wady. W naszym przykładzie zdecydowaliśmy się użyć migratora znaczników czasu Sequela, który wymaga, aby pliki migracji były poprzedzone znacznikiem czasu. Migrator znaczników czasu jest bardzo elastyczny i może akceptować różne formaty znaczników czasu, ale użyjemy tylko tego, który składa się z roku, miesiąca, dnia, godziny, minuty i sekundy. Oto nasze dwa pliki migracji:

 # 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

Jesteśmy teraz gotowi do stworzenia bazy danych ze wszystkimi tabelami.

 bundle exec sequel -m db/migrations sqlite://db/development.sqlite3

Wreszcie w katalogu models mamy pliki modeli sport.rb i player.rb .

 # 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

Tutaj stosujemy metodę Sequel definiowania relacji między modelami, w której obiekt Sport ma wielu graczy, a Player może mieć tylko jeden sport. Ponadto każdy model definiuje swoją metodę to_api , która zwraca skrót z atrybutami, które należy serializować. Jest to ogólne podejście, które możemy zastosować dla różnych formatów. Jeśli jednak użyjemy tylko formatu JSON w naszym API, moglibyśmy użyć Ruby's to_json z argumentem only do ograniczenia serializacji do wymaganych atrybutów, tj player.to_json(only: [:id, :name, :sport_i]) . Oczywiście moglibyśmy również zdefiniować BaseModel , który dziedziczy po Sequel::Model i definiuje domyślną metodę to_api , z której dziedziczą wszystkie modele.

Teraz możemy rozpocząć implementację rzeczywistych punktów końcowych API.

Punkty końcowe API

Zachowamy definicję wszystkich punktów końcowych w plikach w katalogu routes . Ponieważ do wczytywania plików używamy plików manifestu, pogrupujemy trasy według zasobów (tj. wszystkie trasy związane ze sportem przechowujemy w pliku sports.rb , wszystkie trasy graczy w routes.rb itd.).

 # 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

Trasy zagnieżdżone, takie jak ta, w której pobiera się wszystkich graczy w ramach jednego sportu /sports/:id/players , można zdefiniować, umieszczając je razem z innymi trasami lub tworząc oddzielny plik zasobów, który będzie zawierał tylko trasy zagnieżdżone.

Dzięki wyznaczonym trasom aplikacja jest teraz gotowa do przyjmowania żądań:

 curl -i -XGET 'http://localhost:9292/sports?client_id=<client_id>&client_secret=<client_secret>'

Należy zauważyć, że zgodnie z wymaganiami systemu uwierzytelniania aplikacji zdefiniowanego w pliku helpers/authentication.rb poświadczenia przekazujemy bezpośrednio w parametrach żądania.

Powiązane: Samouczek Grape Gem: Jak zbudować API podobne do REST w Ruby

Wniosek

Zasady przedstawione w tej prostej przykładowej aplikacji mają zastosowanie do dowolnej aplikacji zaplecza interfejsu API. Nie jest oparty na architekturze model-widok-kontroler (MVC), ale w podobny sposób zachowuje wyraźny rozdział odpowiedzialności; pełna logika biznesowa jest przechowywana w plikach modeli, podczas gdy obsługa żądań odbywa się w metodach tras Sinatry. W przeciwieństwie do architektury MVC, gdzie do renderowania odpowiedzi wykorzystywane są widoki, ta aplikacja robi to w tym samym miejscu, w którym obsługuje żądania - w metodach tras. Dzięki nowym plikom pomocniczym aplikację można łatwo rozszerzyć o wysyłanie stronicowania lub, w razie potrzeby, żądanie ograniczania informacji z powrotem do użytkownika w nagłówkach odpowiedzi.

W końcu zbudowaliśmy kompletne API z bardzo prostym zestawem narzędzi i bez utraty funkcjonalności. Ograniczona liczba zależności pomaga zapewnić, że aplikacja ładuje się i uruchamia się znacznie szybciej, a także ma znacznie mniejszy ślad pamięciowy niż miałby ten oparty na Railsach. Tak więc następnym razem, gdy zaczniesz pracę nad nowym API w Ruby, rozważ użycie Sinatry i Sequela, ponieważ są to bardzo potężne narzędzia do takiego przypadku użycia.