はじめに
はじめまして。 mochizukiです。
クラシルアプリのサーバーサイドをやってます。
昨日はAndroidエンジニアのumemoriさんが
「マルチモジュール時代のDagger2によるDI」
という記事を書いてくれました。
dely Advent Calendar 2019の2日目は
Netflixがつくった Fast JSON API
について書いてみようと思います。
Fast JSON API
A lightning fast JSON:API serializer for Ruby Objects.
Performance Comparison We compare serialization times with Active Model Serializer as part of RSpec performance tests included on this library. We want to ensure that with every change on this library, serialization time is at least 25 times faster than Active Model Serializers on up to current benchmark of 1000 records. Please read the performance document for any questions related to methodology.
Benchmark times for 250 records
$ rspec Active Model Serializer serialized 250 records in 138.71 ms Fast JSON API serialized 250 records in 3.01 ms
This gem is currently used in several APIs at Netflix and has reduced the response times by more than half on many of these APIs.
使ってみる
まずは準備
ruby 2.6.3 rails 6.0.0
rails new fast_json_api --api
+ gem 'fast_jsonapi'
追加して
class Recipe < ApplicationRecord has_many :ingredients, dependent: :destroy end
class Ingredient < ApplicationRecord belongs_to :recipe end
今回はクラシルっぽくこんな感じで。
中身は
create_table :recipes do |t| t.string :title, null: false t.text :introduction t.timestamps end create_table :ingredients do |t| t.references :recipe, null: false t.string :name, null: false t.string :quantity_and_unit, null: false t.timestamps end
こんな感じで。
データは最近クラシルに実装した
ジャンル別ランキング機能より、殿堂入りの
これにします!
recipes = [ ['ネギダレが美味しい!鶏もも肉のソテー', 'ネギダレが美味しい、鶏もも肉のソテーはいかがでしょうか。'] ] recipes.each_with_index do |array, index| title = array[0] introduction = array[1] Recipe.create({ title: title, introduction: introduction }) end ingredients = [ ['鶏もも肉', '300g'], ['塩こしょう', '小さじ1/4'], ['片栗粉', '大さじ2'], ['長ねぎ', '1本'], ['①しょうゆ', '大さじ1.5'], ['①酢', '小さじ1'], ['①砂糖', '大さじ1'], ['①ごま油', '小さじ1'], ['①白すりごま', '大さじ1'], ['①すりおろし生姜', '小さじ1/2'], ['サラダ油', '大さじ1'], ['リーフレタス', '2枚'] ] Recipe.all.each do |recipe| ingredients.each_with_index do |array, index| name = array[0] quantity_and_unit = array[1] recipe.ingredients.create({ name: name, quantity_and_unit: quantity_and_unit }) end end
そして
class Api::V1::RecipesController < ApplicationController end
こんな感じ。
やっと本題
基本的に ActiveModelSerializers
と同様に使えます。
rails g
で、serializerつくるとこんな感じです。
class RecipeSerializer include FastJsonapi::ObjectSerializer attributes end
なので
class RecipeSerializer include FastJsonapi::ObjectSerializer has_many :ingredients attributes :title, :introduction end
class IngredientSerializer include FastJsonapi::ObjectSerializer belongs_to :recipe attributes :name, :quantity_and_unit end
こんな感じで定義します。
今回つくるAPI
殿堂入りレシピの ネギダレが美味しい!鶏もも肉のソテー
の
レシピ詳細を想定してつくってみます。
GET /api/v1/recipes/:id
class Api::V1::RecipesController < ApplicationController def show recipe = Recipe.find(params[:id]) json_string = RecipeSerializer.new(recipe).serialized_json render json: json_string end end
{ "data": { "id": "1", "type": "recipe", "attributes": { "title": "ネギダレが美味しい!鶏もも肉のソテー", "introduction": "ネギダレが美味しい、鶏もも肉のソテーはいかがでしょうか。" }, "relationships": { "ingredients": { "data": [ { "id": "1", "type": "ingredient" }, { "id": "2", "type": "ingredient" },
(一部のみ表示)
リレーションが紐付いてます。
Started GET "/api/v1/recipes/1" for ::1 at (0.2ms) SELECT sqlite_version(*) Processing by Api::V1::RecipesController#show as JSON Parameters: {"id"=>"1"} Recipe Load (0.3ms) SELECT "recipes".* FROM "recipes" WHERE "recipes"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]] ↳ app/controllers/api/v1/recipes_controller.rb:3:in `show' (0.2ms) SELECT "ingredients"."id" FROM "ingredients" WHERE "ingredients"."recipe_id" = ? [["recipe_id", 1]] ↳ app/controllers/api/v1/recipes_controller.rb:4:in `show' Completed 200 OK in 34ms (Views: 0.6ms | ActiveRecord: 0.5ms | Allocations: 6624)
(キャッシュなしの速度)
材料と分量も返す
レシピ詳細では、材料と分量も返してあげないといけないです。
READMEに従って、
options = { include: %i[ingredients] }
こういうのをつくって
class Api::V1::RecipesController < ApplicationController def show recipe = Recipe.find(params[:id]) options = { include: %i[ingredients] } json_string = RecipeSerializer.new(recipe, options).serialized_json render json: json_string end end
渡してあげます。
{ "data": { "id": "1", "type": "recipe", "attributes": { "title": "ネギダレが美味しい!鶏もも肉のソテー", "introduction": "ネギダレが美味しい、鶏もも肉のソテーはいかがでしょうか。" }, "relationships": { "ingredients": { "data": [ { "id": "1", "type": "ingredient" }, { "id": "2", "type": "ingredient" }, ] } } }, "included": [ { "id": "1", "type": "ingredient", "attributes": { "name": "鶏もも肉", "quantity_and_unit": "300g" }, "relationships": { "recipe": { "data": { "id": "1", "type": "recipe" } } } }, { "id": "2", "type": "ingredient", "attributes": { "name": "塩こしょう", "quantity_and_unit": "小さじ1/4" }, "relationships": { "recipe": { "data": { "id": "1", "type": "recipe" } } } },
(一部のみ表示)
紐付いている材料と分量が返ってきています。
Started GET "/api/v1/recipes/1" for ::1 at (0.2ms) SELECT sqlite_version(*) Processing by Api::V1::RecipesController#show as JSON Parameters: {"id"=>"1"} Recipe Load (0.6ms) SELECT "recipes".* FROM "recipes" WHERE "recipes"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]] ↳ app/controllers/api/v1/recipes_controller.rb:3:in `show' (0.2ms) SELECT "ingredients"."id" FROM "ingredients" WHERE "ingredients"."recipe_id" = ? [["recipe_id", 1]] ↳ app/controllers/api/v1/recipes_controller.rb:5:in `show' Ingredient Load (0.4ms) SELECT "ingredients".* FROM "ingredients" WHERE "ingredients"."recipe_id" = ? [["recipe_id", 1]] ↳ app/controllers/api/v1/recipes_controller.rb:5:in `show' Completed 200 OK in 51ms (Views: 0.4ms | ActiveRecord: 1.2ms | Allocations: 13646)
(キャッシュなしの速度)
キャッシュ
READMEに従って
cache_options enabled: true, cache_length: 1.hours
こんなのつけて
class RecipeSerializer include FastJsonapi::ObjectSerializer cache_options enabled: true, cache_length: 1.hours has_many :ingredients attributes :title, :introduction end
Completed 200 OK in 36ms (Views: 0.3ms | ActiveRecord: 0.8ms | Allocations: 13427) Completed 200 OK in 13ms (Views: 0.2ms | ActiveRecord: 1.4ms | Allocations: 4940)
その他使い方
基本的に ActiveModelSerializers
と同じです。
気になった方は、 README を参照してみてください。
最後に
明日はCXOの坪田さんの「突破するプロダクトマネジメント」です! 楽しみです!!
delyではエンジニアを募集しています。