Как Sequel и Sinatra решают проблему API Ruby
Опубликовано: 2022-03-11Введение
В последние годы количество фреймворков одностраничных приложений и мобильных приложений JavaScript значительно увеличилось. Это налагает соответственно повышенный спрос на серверные API. Поскольку Ruby on Rails является одной из самых популярных на сегодняшний день сред веб-разработки, многие разработчики выбирают ее для создания серверных API-приложений.
Тем не менее, в то время как архитектурная парадигма Ruby on Rails позволяет довольно легко создавать внутренние приложения API, использование Rails только для API является излишним. На самом деле, это излишество до такой степени, что даже команда Rails признала это и поэтому представила новый режим только для API в версии 5. Благодаря этой новой функции в Ruby on Rails создание приложений только для API в Rails стало еще проще. и более жизнеспособный вариант.
Но есть и другие варианты. Наиболее примечательными являются два очень зрелых и мощных драгоценных камня, которые в сочетании предоставляют мощные инструменты для создания серверных API. Это Синатра и Сиквел.
Обе эти жемчужины имеют очень богатый набор функций: Sinatra служит доменным языком (DSL) для веб-приложений, а Sequel служит уровнем объектно-реляционного отображения (ORM). Итак, давайте кратко рассмотрим каждый из них.
Синатра
Sinatra — это стоечная среда веб-приложений. Rack — это хорошо известный интерфейс веб-сервера Ruby. Он используется во многих фреймворках, таких как, например, Ruby on Rails, и поддерживает множество веб-серверов, таких как WEBrick, Thin или Puma. Sinatra предоставляет минимальный интерфейс для написания веб-приложений на Ruby, а одной из его наиболее привлекательных функций является поддержка компонентов промежуточного программного обеспечения. Эти компоненты находятся между приложением и веб-сервером и могут отслеживать запросы и ответы и управлять ими.
Для использования этой функции Rack Sinatra определяет внутренний DSL для создания веб-приложений. Его философия очень проста: маршруты представлены методами HTTP, за которыми следует маршрут, соответствующий шаблону. Блок Ruby, в котором обрабатывается запрос и формируется ответ.
get '/' do 'Hello from sinatra' end
Шаблон сопоставления маршрутов также может включать именованный параметр. Когда блок маршрута выполняется, значение параметра передается в блок через переменную params
.
get '/players/:sport_id' do # Parameter value accessible through params[:sport_id] end
Сопоставление шаблонов может использовать оператор splat *
, который делает значения параметров доступными через params[:splat]
.
get '/players/*/:year' do # /players/performances/2016 # Parameters - params['splat'] -> ['performances'], params[:year] -> 2016 end
На этом возможности Sinatra, связанные с сопоставлением маршрутов, не заканчиваются. Он может использовать более сложную логику сопоставления через регулярные выражения, а также пользовательские сопоставители.
Sinatra понимает все стандартные HTTP-команды, необходимые для создания REST API: Get, Post, Put, Patch, Delete и Options. Приоритеты маршрутов определяются порядком, в котором они определены, и первый маршрут, соответствующий запросу, обслуживает этот запрос.
Приложения Sinatra можно писать двумя способами; используя классический или модульный стиль. Основное различие между ними заключается в том, что в классическом стиле у нас может быть только одно приложение Sinatra для каждого процесса Ruby. Другие отличия настолько незначительны, что в большинстве случаев их можно игнорировать и использовать настройки по умолчанию.
Классический подход
Реализация классического приложения проста. Нам просто нужно загрузить Sinatra и реализовать обработчики маршрутов:
require 'sinatra' get '/' do 'Hello from Sinatra' end
Сохранив этот код в файле demo_api_classic.rb
, мы можем запустить приложение напрямую, выполнив следующую команду:
ruby demo_api_classic.rb
Однако, если приложение будет развертываться с обработчиками Rack, такими как Passenger, лучше запускать его с помощью файла конфигурации Rack config.ru
.
require './demo_api_classic' run Sinatra::Application
При наличии файла config.ru
приложение запускается следующей командой:
rackup config.ru
Модульный подход
Модульные приложения Sinatra создаются путем создания подклассов Sinatra::Base
или Sinatra::Application
:
require 'sinatra' class DemoApi < Sinatra::Application # Application code run! if app_file == $0 end
Оператор, начинающийся с run!
используется для прямого запуска приложения с помощью ruby demo_api.rb
, как и в случае с классическим приложением. С другой стороны, если приложение будет развернуто с помощью Rack, содержимое обработчиков на rackup.ru
должно быть:
require './demo_api' run DemoApi
Продолжение
Sequel — второй инструмент в этом наборе. В отличие от ActiveRecord, который является частью Ruby on Rails, зависимости Sequel очень малы. В то же время он достаточно многофункционален и может использоваться для всех видов задач по работе с базами данных. Благодаря простому предметно-ориентированному языку Sequel избавляет разработчика от всех проблем, связанных с поддержанием соединений, построением SQL-запросов, извлечением данных из базы данных (и отправкой данных обратно в нее).
Например, установить соединение с базой данных очень просто:
DB = Sequel.connect(adapter: :postgres, database: 'my_db', host: 'localhost', user: 'db_user')
Метод connect возвращает объект базы данных, в данном случае Sequel::Postgres::Database
, который в дальнейшем можно использовать для выполнения необработанного SQL.
DB['select count(*) from players']
В качестве альтернативы, чтобы создать новый объект набора данных:
DB[:players]
Оба этих утверждения создают объект набора данных, который является базовой сущностью Sequel.
Одной из наиболее важных функций набора данных Sequel является то, что он не выполняет запросы немедленно. Это позволяет сохранять наборы данных для последующего использования и, в большинстве случаев, объединять их в цепочки.
users = DB[:players].where(sport: 'tennis')
Итак, если набор данных не сразу попадает в базу данных, возникает вопрос, когда это произойдет? Sequel выполняет SQL в базе данных, когда используются так называемые «исполняемые методы». Вот некоторые из этих методов: all
, each
, map
, first
и last
.
Sequel является расширяемым, и его расширяемость является результатом фундаментального архитектурного решения построить небольшое ядро, дополненное системой плагинов. Функции легко добавляются с помощью плагинов, которые на самом деле являются модулями Ruby. Наиболее важным плагином является плагин Model
. Это пустой плагин, который сам по себе не определяет никаких методов класса или экземпляра. Вместо этого он включает в себя другие плагины (подмодули), которые определяют методы набора данных класса, экземпляра или модели. Плагин модели позволяет использовать Sequel в качестве инструмента объектно-реляционного сопоставления (ORM) и часто упоминается как «базовый плагин».
class Player < Sequel::Model end
Модель Sequel автоматически анализирует схему базы данных и устанавливает все необходимые методы доступа для всех столбцов. Предполагается, что имя таблицы во множественном числе и является подчеркнутой версией имени модели. В случае необходимости работы с базами данных, которые не следуют этому соглашению об именовании, имя таблицы может быть задано явно при определении модели.
class Player < Sequel::Model(:player) end
Итак, теперь у нас есть все необходимое, чтобы приступить к созданию серверного API.
Создание API
Структура кода
В отличие от Rails, Sinatra не навязывает структуру проекта. Однако, поскольку организация кода для облегчения обслуживания и разработки всегда является хорошей практикой, мы сделаем это и здесь со следующей структурой каталогов:

project root |-config |-helpers |-models |-routes
Конфигурация приложения будет загружена из файла конфигурации YAML для текущей среды с помощью:
Sinatra::Application.config_file File.join(File.dirname(__FILE__), 'config', "#{Sinatra::Application.settings.environment}_config.yml")
По умолчанию значением Sinatra::Applicationsettings.environment
является development,
и оно изменяется путем установки переменной среды RACK_ENV
.
Кроме того, наше приложение должно загрузить все файлы из трех других каталогов. Мы можем сделать это легко, запустив:
%w{helpers models routes}.each {|dir| Dir.glob("#{dir}/*.rb", &method(:require))}
На первый взгляд такой способ загрузки может показаться удобным. Однако с помощью этой одной строки кода мы не можем легко пропустить файлы, потому что будут загружены все файлы из каталогов в массиве. Вот почему мы будем использовать более эффективный подход загрузки одного файла, который предполагает, что в каждой папке у нас есть файл манифеста init.rb
, который загружает все остальные файлы из каталога. Кроме того, мы добавим целевой каталог в путь загрузки 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
Этот подход требует немного больше работы, потому что мы должны поддерживать операторы require в каждом файле init.rb
, но, в свою очередь, мы получаем больше контроля, и мы можем легко исключить один или несколько файлов, удалив их из файла манифеста init.rb
в целевом каталоге.
API-аутентификация
Первое, что нам нужно в каждом API, — это аутентификация. Мы реализуем его как вспомогательный модуль. Полная логика аутентификации будет в файле 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
Все, что нам нужно сделать сейчас, это загрузить этот файл, добавив оператор require в файл манифеста помощника ( helpers/init.rb
) и вызвать функцию authenticate!
в хуке before
Sinatra, который будет выполняться перед обработкой любого запроса.
before do authenticate! end
База данных
Далее нам нужно подготовить нашу базу данных для приложения. Есть много способов подготовить базу данных, но, поскольку мы используем Sequel, естественно сделать это с помощью миграции. Sequel поставляется с двумя типами миграции — целочисленным и на основе временных меток. Каждый из них имеет свои преимущества и недостатки. В нашем примере мы решили использовать миграционную метку времени Sequel, которая требует, чтобы файлы миграции имели префикс временной метки. Мигратор временных меток очень гибкий и может принимать различные форматы временных меток, но мы будем использовать только тот, который состоит из года, месяца, дня, часа, минуты и секунды. Вот наши два файла миграции:
# 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
Теперь мы готовы создать базу данных со всеми таблицами.
bundle exec sequel -m db/migrations sqlite://db/development.sqlite3
Наконец, у нас есть файлы sport.rb
и player.rb
в каталоге 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
Здесь мы используем метод Sequel для определения отношений модели, где объект Sport
имеет много игроков, а Player
может иметь только один вид спорта. Также каждая модель определяет свой метод to_api
, который возвращает хеш с атрибутами, которые необходимо сериализовать. Это общий подход, который мы можем использовать для различных форматов. Однако, если мы будем использовать только формат JSON в нашем API, мы могли бы использовать Ruby to_json
с only
аргументом, чтобы ограничить сериализацию необходимыми атрибутами, то есть player.to_json(only: [:id, :name, :sport_i])
. Конечно, мы могли бы также определить BaseModel
, которая наследуется от Sequel::Model
, и определяет метод to_api
по умолчанию, от которого могут наследоваться все модели.
Теперь мы можем приступить к реализации реальных конечных точек API.
Конечные точки API
Мы сохраним определение всех конечных точек в файлах в каталоге routes
. Поскольку мы используем файлы манифеста для загрузки файлов, мы сгруппируем маршруты по ресурсам (т. е. сохраняем все маршруты, связанные со спортом, в файле sports.rb
, маршруты всех игроков — в routes.rb
и т. д.).
# 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
Вложенные маршруты, например, для получения всех игроков в одном виде спорта /sports/:id/players
, можно определить, поместив их вместе с другими маршрутами, или создав отдельный файл ресурсов, который будет содержать только вложенные маршруты.
С назначенными маршрутами приложение теперь готово принимать запросы:
curl -i -XGET 'http://localhost:9292/sports?client_id=<client_id>&client_secret=<client_secret>'
Обратите внимание, что в соответствии с требованиями системы аутентификации приложения, определенной в файле helpers/authentication.rb
, мы передаем учетные данные непосредственно в параметрах запроса.
Заключение
Принципы, продемонстрированные в этом простом примере приложения, применимы к любому серверному приложению API. Он не основан на архитектуре модель-представление-контроллер (MVC), но аналогичным образом сохраняет четкое разделение обязанностей; полная бизнес-логика хранится в файлах модели, а обработка запросов выполняется в методах маршрутов Sinatra. В отличие от архитектуры MVC, где для рендеринга ответов используются представления, это приложение делает это там же, где и обрабатывает запросы — в методах маршрутов. С помощью новых вспомогательных файлов приложение можно легко расширить для отправки разбивки на страницы или, при необходимости, запроса информации об ограничениях обратно пользователю в заголовках ответов.
В итоге мы создали полноценный API с очень простым набором инструментов и без потери функциональности. Ограниченное количество зависимостей помогает гарантировать, что приложение будет загружаться и запускаться намного быстрее и будет занимать гораздо меньше памяти, чем приложение на основе Rails. Итак, в следующий раз, когда вы начнете работать над новым API в Ruby, рассмотрите возможность использования Sinatra и Sequel, поскольку они являются очень мощными инструментами для такого случая использования.