Sequel 和 Sinatra 如何解決 Ruby 的 API 問題
已發表: 2022-03-11介紹
近年來,JavaScript 單頁應用程序框架和移動應用程序的數量大幅增加。 這對服務器端 API 的需求相應增加。 Ruby on Rails 是當今最流行的 Web 開發框架之一,它是許多開發人員創建後端 API 應用程序的自然選擇。
然而,儘管 Ruby on Rails 架構範式使得創建後端 API 應用程序變得相當容易,但僅將 Rails 用於 API 是多餘的。 事實上,甚至 Rails 團隊都意識到了這一點,因此在第 5 版中引入了一種新的僅 API 模式,這太過分了。借助 Ruby on Rails 中的這一新功能,在 Rails 中創建僅 API 應用程序變得更加容易和更可行的選擇。
但也有其他選擇。 最值得注意的是兩個非常成熟且功能強大的 gem,它們結合起來為創建服務器端 API 提供了強大的工具。 他們是辛納屈和續集。
這兩個 gem 都具有非常豐富的功能集:Sinatra 用作 Web 應用程序的領域特定語言 (DSL),而 Sequel 用作對象關係映射 (ORM) 層。 因此,讓我們簡要介紹一下它們中的每一個。
辛納特拉
Sinatra 是基於 Rack 的 Web 應用程序框架。 Rack 是一個眾所周知的 Ruby Web 服務器界面。 它被許多框架使用,例如 Ruby on Rails,並支持許多 Web 服務器,例如 WEBrick、Thin 或 Puma。 Sinatra 為用 Ruby 編寫 Web 應用程序提供了一個最小接口,它最引人注目的特性之一是對中間件組件的支持。 這些組件位於應用程序和 Web 服務器之間,可以監視和操作請求和響應。
為了利用這個 Rack 特性,Sinatra 定義了用於創建 Web 應用程序的內部 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 了解創建 REST API 所需的所有標準 HTTP 動詞:Get、Post、Put、Patch、Delete 和 Options。 路由優先級由定義它們的順序決定,第一個匹配請求的路由就是為該請求提供服務的路由。
Sinatra 應用程序可以用兩種方式編寫; 使用古典或模塊化風格。 它們之間的主要區別在於,在經典風格下,每個 Ruby 進程只能有一個 Sinatra 應用程序。 其他差異很小,在大多數情況下,可以忽略它們,並且可以使用默認設置。
經典方法
實現經典應用程序很簡單。 我們只需要加載 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 是該集合中的第二個工具。 與作為 Ruby on Rails 一部分的 ActiveRecord 相比,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
這種方法需要更多的工作,因為我們必須在每個init.rb
文件中維護 require 語句,但作為回報,我們獲得了更多控制權,並且我們可以通過從清單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
我們現在要做的就是通過在幫助程序清單文件 ( helpers/init.rb
) 中添加一個 require 語句來加載這個文件並調用authenticate!
Sinatra 的before
鉤子中的方法,它將在處理任何請求之前執行。
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
最後,我們在models
目錄中有模型文件sport.rb
和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
在這裡,我們採用 Sequel 方式來定義模型關係,其中Sport
對像有許多玩家,而Player
只能有一項運動。 此外,每個模型都定義了它的to_api
方法,該方法返回一個帶有需要序列化的屬性的散列。 這是我們可以用於各種格式的通用方法。 但是,如果我們只在 API 中使用 JSON 格式,我們可以使用 Ruby 的to_json
和only
的參數來限制序列化到所需的屬性,即player.to_json(only: [:id, :name, :sport_i])
。 當然,我們也可以定義一個繼承自Sequel::Model
的BaseModel
並定義一個默認的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 的應用程序具有更小的內存佔用。 因此,下次您開始使用 Ruby 開發新 API 時,請考慮使用 Sinatra 和 Sequel,因為它們對於此類用例來說是非常強大的工具。