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 和 Sequel 的 API:Ruby 教程

Ruby API 节食:介绍 Sequel 和 Sinatra。
鸣叫

辛纳特拉

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::BaseSinatra::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。 仅举几例,这些方法是alleachmapfirstlast

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.rbplayer.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_jsononly的参数来限制序列化到所需的属性,即player.to_json(only: [:id, :name, :sport_i]) 。 当然,我们也可以定义一个继承自Sequel::ModelBaseModel并定义一个默认的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文件中定义的应用程序身份验证系统的要求,我们直接在请求参数中传递凭据。

相关: Grape Gem 教程:如何在 Ruby 中构建类似 REST 的 API

结论

这个简单示例应用程序中展示的原则适用于任何 API 后端应用程序。 它不是基于模型-视图-控制器(MVC)架构,但它以类似的方式保持清晰的职责分离; 完整的业务逻辑保存在模型文件中,而处理请求则在 Sinatra 的路由方法中完成。 与使用视图呈现响应的 MVC 架构相反,此应用程序在处理请求的同一位置执行此操作 - 在路由方法中。 使用新的帮助文件,应用程序可以轻松扩展以发送分页,或者,如果需要,请求限制信息在响应标头中返回给用户。

最后,我们用一个非常简单的工具集构建了一个完整的 API,并且没有丢失任何功能。 有限数量的依赖项有助于确保应用程序加载和启动更快,并且比基于 Rails 的应用程序具有更小的内存占用。 因此,下次您开始使用 Ruby 开发新 API 时,请考虑使用 Sinatra 和 Sequel,因为它们对于此类用例来说是非常强大的工具。