SequelとSinatraがRubyのAPI問題をどのように解決するか

公開: 2022-03-11

序章

近年、JavaScriptシングルページアプリケーションフレームワークとモバイルアプリケーションの数が大幅に増加しています。 これにより、それに応じてサーバー側APIの需要が増加します。 Ruby on Railsは今日最も人気のあるWeb開発フレームワークの1つであり、バックエンドAPIアプリケーションを作成するための多くの開発者の間で当然の選択です。

Ruby on RailsのアーキテクチャパラダイムではバックエンドAPIアプリケーションの作成が非常に簡単ですが、RailsをAPIにのみ使用するのはやり過ぎです。 実際、Railsチームでさえこれを認識し、バージョン5で新しいAPIのみのモードを導入したという点ではやり過ぎです。RubyonRailsのこの新機能により、RailsでのAPIのみのアプリケーションの作成がさらに簡単になりました。そしてより実行可能なオプション。

しかし、他のオプションもあります。 最も注目に値するのは、2つの非常に成熟した強力なgemであり、これらを組み合わせることで、サーバー側APIを作成するための強力なツールを提供します。 彼らはシナトラと続編です。

これらの宝石は両方とも非常に豊富な機能セットを備えています。SinatraはWebアプリケーションのドメイン固有言語(DSL)として機能し、Sequelはオブジェクトリレーショナルマッピング(ORM)レイヤーとして機能します。 それでは、それぞれについて簡単に見てみましょう。

SinatraとSequelを使用したAPI:Rubyチュートリアル

ダイエット中のRubyAPI:SequelとSinatraの紹介。
つぶやき

シナトラ

Sinatraは、ラックベースのWebアプリケーションフレームワークです。 Rackは、よく知られているRubyWebサーバーインターフェイスです。 たとえば、Ruby on Railsなどの多くのフレームワークで使用され、WEBrick、Thin、Pumaなどの多くのWebサーバーをサポートします。 Sinatraは、RubyでWebアプリケーションを作成するための最小限のインターフェースを提供し、その最も魅力的な機能の1つは、ミドルウェアコンポーネントのサポートです。 これらのコンポーネントは、アプリケーションとWebサーバーの間にあり、要求と応答を監視および操作できます。

このラック機能を利用するために、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は、REST APIの作成に必要なすべての標準HTTP動詞(Get、Post、Put、Patch、Delete、およびOptions)を理解しています。 ルートの優先順位は、定義された順序によって決定され、要求に一致する最初のルートがその要求を処理するルートになります。

Sinatraアプリケーションは2つの方法で作成できます。 クラシックまたはモジュラースタイルを使用します。 それらの主な違いは、クラシックスタイルでは、Rubyプロセスごとに1つのSinatraアプリケーションしか持てないことです。 その他の違いはごくわずかであるため、ほとんどの場合、無視してデフォルト設定を使用できます。

古典的なアプローチ

従来のアプリケーションの実装は簡単です。 Sinatraをロードし、ルートハンドラーを実装するだけです。

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

このコードをdemo_api_classic.rbファイルに保存すると、次のコマンドを実行してアプリケーションを直接起動できます。

 ruby demo_api_classic.rb

ただし、PassengerなどのRackハンドラーを使用してアプリケーションをデプロイする場合は、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

続編

続編は、このセットの2番目のツールです。 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データセットの最も重要な機能の1つは、クエリをすぐに実行しないことです。 これにより、後で使用するためにデータセットを保存し、ほとんどの場合、それらをチェーンすることができます。

 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環境変数を設定することで変更されます。

さらに、アプリケーションは他の3つのディレクトリからすべてのファイルをロードする必要があります。 これは、次のコマンドを実行することで簡単に実行できます。

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

一見すると、このロード方法は便利に見えるかもしれません。 ただし、この1行のコードでは、配列内のディレクトリからすべてのファイルが読み込まれるため、ファイルを簡単にスキップすることはできません。 そのため、より効率的な単一ファイルのロードアプローチを使用します。これは、各フォルダーにマニフェストファイル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ファイルから1つ以上のファイルを削除することで簡単に除外できます。ターゲットディレクトリにあります。

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には、整数ベースとタイムスタンプベースの2つの移行タイプがあります。 それぞれに長所と短所があります。 この例では、Sequelのタイムスタンプ移行ツールを使用することにしました。これには、移行ファイルのプレフィックスにタイムスタンプを付ける必要があります。 タイムスタンプ移行機能は非常に柔軟性があり、さまざまなタイムスタンプ形式を受け入れることができますが、使用するのは年、月、日、時、分、秒の形式のみです。 2つの移行ファイルは次のとおりです。

 # 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

ここでは、モデルの関係を定義する続編の方法を採用しています。ここでは、 Sportオブジェクトには多くのプレーヤーがいて、 Playerは1つのスポーツしか持てません。 また、各モデルは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

1つのスポーツ/sports/:id/players 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バックエンドアプリケーションに適用されます。 これは、model-view-controller(MVC)アーキテクチャに基づいていませんが、同様の方法で責任を明確に分離しています。 リクエストの処理がSinatraのroutesメソッドで行われている間、完全なビジネスロジックはモデルファイルに保持されます。 ビューが応答のレンダリングに使用されるMVCアーキテクチャとは異なり、このアプリケーションは、ルートメソッドでリクエストを処理するのと同じ場所でそれを実行します。 新しいヘルパーファイルを使用すると、アプリケーションを簡単に拡張して、ページ付けを送信したり、必要に応じて、応答ヘッダーでユーザーに制限情報を要求したりできます。

最終的に、機能を失うことなく、非常にシンプルなツールセットを使用して完全なAPIを構築しました。 依存関係の数が限られているため、アプリケーションのロードと起動がはるかに高速になり、Railsベースのアプリケーションよりもメモリフットプリントがはるかに小さくなります。 したがって、次にRubyで新しいAPIの作業を開始するときは、SinatraとSequelを使用することを検討してください。これらは、このようなユースケースにとって非常に強力なツールです。