Rubyの5つの活用例
Rubyで何ができるのだろうと思ったことはないだろうか。まあ、可能性は無限大でしょうが、多少なりとも知られている事例についてお話しできれば幸いです。
著者データを含む(または含まない)本をリストアップする本棚アプリを作ります。
著者データを含む(または含まない)本をリストアップする本棚アプリを作ります。単一の 1TP63インデックス
アクションといくつかのシード。このアプリは、どのようにしてユーザーにアクションをコントロールさせることができるかを示すサンプルアプリとなる。 REST的APIのサブリソース.
を含む。
クエリ・パラメータを使用して、関連するリソース (著者
).を含む。
クエリーパラメーターは、カンマで区切られた文字列で、ネストされたリソースを表します。を使用する。 ブループリンター
をシリアライザとして使用することができます。これはrailsの標準ツールセットに追加する唯一のgemです。
サンプルアプリを作ってみよう。テスト・フレームワークは私たちの範囲外なので追加しない。
新しい本棚
今すぐ作成する 著者
モデルである:
モデル作者名:文字列
#=> active_recordを呼び出す
#=> db/migrate/20211224084524_create_authors.rb を作成する。
#=>作成 app/models/author.rb
そして 書籍
:
rails g モデル book author:references title:string
# => active_recordを呼び出す
# => create db/migrate/20211224084614_create_books.rb
# => app/models/book.rbの作成
種が必要だ:
# db/seeds.rb
dumas = Author.create(名前: 'Alexandre Dumas')
lewis = Author.create(名前: 'C.S. Lewis')
martin = Author.create(名前: 'ロバート・C・マーティン')
Book.create(author: dumas, title: '三銃士')
Book.create(author: lewis, title: 'ライオンと魔女と衣装だんす')
Book.create(著者:マーティン、タイトル:『クリーン・コード』)
これでマイグレーションを実行し、データベースをシードする準備ができた:
rails db:migrate && rails db:seed
追加しよう has_many
の本のために 著者
モデルである:
# app/models/author.rb
class Author < ApplicationRecord
has_many :books
終了
データを返すコントローラを書きましょう。ここでは API
という名前空間があるので、まずは抑揚に頭文字を追加してみよう:
# config/initializers/inflections.rb
ActiveSupport::Inflector.inflections(:en) do |inflect|.
inflect.acronym 'API'
終了
では、シリアライザーを ジェムファイル
:
# Gemfile に追加
gem 'blueprinter'
そしてもちろん、それをインストールする:
バンドルインストール
そうすれば、設計図を作ることができる:
# app/blueprints/author_blueprint.rb
class AuthorBlueprint < ブループリンター::ベース
識別子 :id
フィールド :name
終了
# app/blueprints/book_blueprint.rb
class BookBlueprint < ブループリンター::ベース
識別子 :id
フィールド :title
関連 :author, ブループリント:著者ブループリント
終了
のベースコントローラーを追加する。 API
:
# app/controllers/api/v1/base_controller.rb
モジュール API
モジュール V1
class BaseController < ActionController::API
end
終了
終わり
そして、私たちの草案 ブックスコントローラー
:
# app/controllers/api/v1/books_controller.rb
モジュール API
モジュール V1
class BooksController < BaseController
def index
Books = Book.all
jsonをレンダリングするBookBlueprint.render(books)
終了
終了
終了
終了
もちろん、ルーティングも定義しなければならない:
# config/routes.rb
Rails.application.routes.draw do
名前空間 :api do
名前空間 :v1 do
リソース :books, only: :index
end
end
終了
これまでやってきたことをテストしてみよう:
レール
curl http://localhost:3000/api/v1/books
# => [{"id":1, "author":{"id":1, "name": "Alexandre Dumas"}, "title": "三銃士"},{"id":2, "author":{"id":2, "name": "C.id": "3", "author":"{"id": "3", "name": "ロバート・C・マーティン"}, "title": "クリーン" コード"}]
データは問題ないようだが、ログはどうなのか?
#リクエストログ(n+1)
GET "/api/v1/books" for 127.0.0.1 at 2021-12-24 10:19:40 +0100を開始しました。
API::V1::BooksController#indexによる処理 */*として
ブックのロード (0.1ms) SELECT "books".* FROM "books"
↳ app/controllers/api/v1/books_controller.rb:7:in `index'
著者ロード (0.1ms) SELECT "authors".* FROM "authors" WHERE "authors". "id" = ?LIMIT ? [["id", 1], ["LIMIT", 1] ]。
↳ app/controllers/api/v1/books_controller.rb:7:in `index'.
著者ロード (0.1ms) SELECT "authors".* FROM "authors" WHERE "authors". "id" = ?LIMIT ? [["id", 2], ["LIMIT", 1] ]。
↳ app/controllers/api/v1/books_controller.rb:7:in `index'.
著者のロード (0.1ms) SELECT "authors".* FROM "authors" WHERE "authors". "id" = ?LIMIT ? [["id", 3], ["LIMIT", 1] ]。
↳ app/controllers/api/v1/books_controller.rb:7:in `index'.
6ms で 200 OK を完了 (Views: 0.1ms | ActiveRecord: 0.4ms | Allocations: 3134)
シリアライザーにアソシエーションを使用することで、次のようになりました。 n+1
問題である。このエンドポイントに何をリクエストするかをユーザーがコントロールできるようにすることで、この問題を解消したい。つまり、ブックだけをロードすることも、includesパラメータを渡して著者も取得することもできるようにしなければなりません。 n+1
.
この定数を定義して、ユーザーがどのような本のassocを books1TP63インデックス
アクションだ:
# lib/constants/books/includes.rb
モジュール Constants
モジュール Books
モジュール Includes
ALLOWED = { (許可される)
インデックス%i[(本)
著者
フリーズ
フリーズ
終了
終了
終了
次に、空のオブジェクト定数用の名前空間を定義する:
# lib/constants/empty.rb
モジュール定数
モジュールEmpty
HASH = {}.freeze
終了
終了
そして、これがインクルードを許可するためのメイン・サービスだ。このコードはとてもわかりやすいと思います。 マジック
にのみ割り当てられる。 #default_resources_key
そして #デフォルトの目的
.これらのメソッドは、railsのコントローラでパラメータのみを渡すpermit includeを呼び出せるように定義されています。出力は 真の
許可されたインクルージョンごとに。
# app/services/permit_includes.rb
require 'constants/empty'
require 'constants/books/includes'.
クラス PermitIncludes
空 = 定数::空
COMMA = ','
スラッシュ = '/'
INCLUDES_FORMAT = /A[a-z]+(,[a-z]+)*z/.freeze
allowed_includes = {(許可されたインクルード
書籍:定数::ブック::インクルード::ALLOWED
.フリーズ
def call(params, resources: default_resources_key(params), purpose: default_purpose(params))
includes_sent?(params)でなければEmpty::HASHを返す。
includes_valid?(params)でなければEmpty::HASHを返す
requested_includes = parse_includes(params)
allowed_includes = filter_includes(requested_includes, resources, purpose)
allowed_includes.index_with(true)
終了
プライベート
def default_resources_key(params)
raise(ArgumentError, 'params :controller key must be a string') unless params[:controller].is_a?(String)
params[:controller].split(SLASH).last&.to_sym
終了
def default_purpose(params)
raise(ArgumentError, 'params :action key must be a string') unless params[:action].is_a?(String)
params[:action].to_sym
終了
def includes_sent?(params)
params.key?(:includes)
end
def includes_valid?(params)
params[:includes].is_a?(String)でなければfalseを返す
params[:includes].match?(INCLUDES_FORMAT)
終了
def parse_includes(params)
params[:includes].split(COMMA).map(&:to_sym)
end
def filter_includes(requested_includes, resources_key, purpose)
requested_includes & ALLOWED_INCLUDES[resources_key][purpose].
end
エンド
ここで、インクルードをロードするためにキーを使用し、インクルードのハッシュ自体をシリアライザーに渡す必要がある:
# app/controllers/api/v1/books_controller.rb
モジュール API
モジュール V1
class BooksController < BaseController
def index
includes = PermitIncludes.new.call(params)
books = Book.includes(includes.keys).all
jsonをレンダリングする:BookBlueprint.render(books, includes: includes)
終了
終了
終了
終了
そして、このようにシリアライザーを調整しなければならない:
# app/blueprints/book_blueprint.rb
class BookBlueprint (_field_name, _book, options) { オプション[:includes
options[:includes] && options[:includes][:author].
}
終了
もう一度テストしてみよう:
レール
カール http://localhost:3000/api/v1/books
# => [{"id":1, "title": "The Three Musketeers"},{"id":2, "title": "The Lion, the Witch and the Wardrobe"},{"id":3, "title": "Clean Code"}]。
#のリクエストログ(書籍のみ読み込みます)
GET "/api/v1/books" for ::1 at 2021-12-24 10:33:41 +0100を開始しました。
API::V1::BooksController#indexで*/*として処理。
(0.1ms) SELECT sqlite_version(*)
↳ app/controllers/api/v1/books_controller.rb:8:in `index'.
ブックのロード (0.1ms) SELECT "books".* FROM "books"
↳ app/controllers/api/v1/books_controller.rb:8:in `index'
9ms で 200 OK を完了 (Views: 0.1ms | ActiveRecord: 0.9ms | Allocations: 4548)
よし、まだincludesを通過していないので、著者のいない本だけを手に入れた。では、リクエストしてみよう:
curl 'http://localhost:3000/api/v1/books?includes=author'
id":2, "author":{"id":2, "name": "C.S.ルイス"}, "title": "ライオンと魔女と衣装だんす" },{"id":3, "author":{"id":3, "name": "Robert C. Martin"}, "title": "クリーン・コード" }]%
#のリクエストログ(n+1を排除)
GET "/api/v1/books?includes=author "を開始しました。
としてAPI::V1::BooksController#indexで処理する。
パラメータ{"includes"=>"author"}
ブックのロード (0.1ms) SELECT "books".* FROM "books"
↳ app/controllers/api/v1/books_controller.rb:8:in `index'
著者のロード (0.2ms) SELECT "authors".* FROM "authors" WHERE "authors". "id" IN (?, ?, ?) [["id", 1], ["id", 2], ["id", 3]].
↳ app/controllers/api/v1/books_controller.rb:8:in `index'.
完了 200 OK in 17ms (Views: 0.1ms | ActiveRecord: 0.7ms | Allocations: 7373)
クールだ!我々は、協会をロードし、排除した n+1
問題です。このサービスはどんなリソースにも使うことができる。私たちがしたいことは、許可された定数を適切な形式で追加し、それを PermitIncludes::ALLOWED_INCLUDES
.
関連付けを含むと多くのメモリを "消費 "する可能性があるため、ページネーションと併用する(そして注意する)必要があることを忘れてはならない。