はじめに
ActiveModelSerializerを業務で使ったのでざっくりと使い方をまとめようと思います。
誤り等ございましたら、コメントいただけるとありがたいです。
セットアップ
インストール方法
- Gemのインストール手順
出力されるjson構造を変更したい場合(defaultはattributes),
gem 'active_model_serializers', '~> 0.10.0'
config/initializers/active_model_serializers.ファイルを作成し,を記述します。ActiveModelSerializers.config.adapter = :json_api
active_model_serializerとは
- ActiveModelSerializerの概要
ActiveModelSerializerはrailsアプリケーションを簡単にjsonシリアライズするためのgemです
シリアライズとは、(Wikipediaさんより)
プログラミング用語として:一つまたは複数のデータ、ファイル、あるいは一つまたは複数のオブジェクトといった、概念的あるいは事実上、複数の別のものとして取り扱っているエンティティを、例えばネットワーク経由で転送する、ストレージに一時格納するなどの目的で、「一つのまとまりとして取り扱う必要」がある場合、「階層をもたないフラットな(直線的な)データ構造に変換する」こと。訳語は直列化。オブジェクト指向プログラミングでは同義語としてマーシャリング (marshalling) がある。対義語はデシリアライズ(訳語は直列化復元)である。
人が見やすいように、一つのまとまりに変えることなのかなと。
つまり、ActiveModelSerializerとはデータをわかりやすく一つのまとまったJSON形式に変換するgemです
使い方
- 使わなかった場合
Userモデル(id, name, email, phone_number, created_at...)を持ったオブジェクトがあるとします。さらに1対NのPostモデル(id, title, content)のリレーションを持っているとします。
それをid, name, created_atと関連づけられたpostのみ json形式で渡すとすると
class UserController < ApplicationController
def show
user = User.includes(:posts).find(params[:id])
render json: user.as_json(
only: [:id, :name, :email, :created_at],
include: {
posts: {
only: [:id, :content, :created_at]
}
}
)
end
end
と記述できます。使わない情報は渡さないこともできます。(phone_numberは渡さないということもできる)
ただこのままだと、さらにcommentというpostに紐づけられたオブジェクトも渡したいとなるとネストが深くなり、,,,,
さらに、コードの再利用性はないため冗長になりやすいです
- ActiveModelSerializerを使う場合
まず、Userモデルに対応するSerializerクラスを定義しますclass UserSerializer < ActiveModel::Serializer # 渡す値を記入 attributes :id, :name, :email, :created_at has_many :posts end class PostSerializer < ActiveModel::Serializer attributes :id, :content, :created_at end class UserController < ApplicationController def show user = User.includes(:posts).find(params[:id]) # 自動的にUserSerializerを使われる render json user end end
また、コードの再利用性を考えてUser ControllerのindexメソッドにもSerializerを適用すると
class UserSerializer < ActiveModel::Serializer
# 渡す値を記入
attributes :id, :name
attribute :email, if: :attached_email?
has_many :posts
def attached_email
instance_options[:attached_email]
end
end
class PostSerializer < ActiveModel::Serializer
# テーブルのすべてのカラムをシリアライズする
attributes(*Post.column_names)
end
class UserController < ApplicationController
def index
user = User.includes(:posts)
render json user, status: :ok
end
def show
user = User.includes(:posts).find(params[:id])
# 自動的にUserSerializerを使われる
render json user, status: :ok, attached_email: true
end
end
このように記述することでattached_emailがtrueの場合のみemailを渡すこともでき、コントローラのメソッドによってjsonの値を使い分けることもできます。
パフォーマンス測定
普通に考えたら、渡すデータを制限した方が速度向上につながるようなイメージですが、本当にパフォーマンスが改善されているのか、と思うので実際に測定を行います。
rails gemのfakerを使ってseed.rbを作成しました。
以下、seedファイルの内容
# Create 5 users
5.times do |i|
user = User.create!(
provider: "email",
uid: Faker::Internet.uuid,
encrypted_password: Faker::Internet.password(min_length: 8),
name: Faker::Name.name,
nickname: Faker::Internet.username,
email: Faker::Internet.email,
phone_number: Faker::PhoneNumber.cell_phone_in_e164,
introduction: Faker::Lorem.sentence,
location: Faker::Address.city,
website: Faker::Internet.url,
date_of_birth: Faker::Date.birthday(min_age: 18, max_age: 65),
password: 'hogehoge',
confirmed_at: Time.now
)
puts "Created user: #{user.name}"
# Create 20 tweets for each user
20.times do
tweet = Tweet.new(
content: Faker::Lorem.paragraph(sentence_count: 2, supplemental: false, random_sentences_to_add: 4),
user: user
)
# 50%の確率で1~3枚の画像を添付
if rand < 0.5
rand(1..3).times do
tweet.images.attach(
io: StringIO.new(Faker::Lorem.paragraph),
filename: "#{Faker::Lorem.word}.jpg",
content_type: 'image/jpeg'
)
end
end
tweet.save!
puts "Created tweet for #{user.name}"
end
end
puts "Seeding completed! Created #{User.count} users and #{Tweet.count} tweets."
元からあるUserとTweetの数を足した結果がUser数: 8, Tweet数: 103.
これで速度測定を行います。
実験方法
rubyにdefalutであるbenchmarkを使ってレスポンス速度を測定します
- Serializerなし,データ全渡し
- Serializerなし,データ絞り込み渡し
- Serializerあり、データ絞り込み渡し
これを参考にさせていただきました。
速度はtotalで比較しました。
1と2で渡すデータを少なくするとレスポンス速度が上がるかの検証を行い、2と3でActiveModelSerializerを使うことによるレスポンス速度が上がるかの検証を行います。
想定場面としては、Twitterの投稿一覧画面を想定場面としています。
結果
- Serializerなし,データ全渡し
def index result = Benchmark.measure do tweets = Tweet.includes(:user).order(updated_at: 'DESC') render json: tweets, include: [:user], methods: [:image_url], status: :ok end Rails.logger.info "Total new action execution time: #{result.total} seconds" end
回数 | 時間 (秒) |
---|---|
初回 | 0.318343 |
リロード | 0.290193 |
2 | 0.267181 |
3 | 0.290688 |
4 | 0.267231 |
- Serializerなし,データ絞り込み渡し
def index
result = Benchmark.measure do
tweets = Tweet.includes(:user).order(updated_at: 'DESC')
render json: tweets.as_json(only: %i[id content created_at],
include: { user: { only: %i[id name] } },
methods: [:image_url]), status: :ok
end
Rails.logger.info "Total new action execution time: #{result.total} seconds"
end
回数 | 時間 (秒) |
---|---|
初回 | 0.306811 |
リロード | 0.278567 |
2 | 0.252422 |
3 | 0.281951 |
4 | 0.276592 |
- Serializerあり、データ絞り込み渡し
def index
result = Benchmark.measure do
tweets = Tweet.includes(:user).order(updated_at: 'DESC')
render json: tweets, status: :ok, attached_images: true
end
Rails.logger.info "Total new action execution time: #{result.total} seconds"
end
class UserSerializer < ActiveModel::Serializer
attributes :id, :name
has_many :tweets
end
class TweetSerializer < ActiveModel::Serializer
include Rails.application.routes.url_helpers
attributes :id, :content, :created_at, :images
belongs_to :user
def images
return [] unless object.images.attached?
object.images.map { |image| url_for(image) }
end
end
回数 | 時間 (秒) |
---|---|
初回 | 0.322347 |
リロード | 0.291365 |
2 | 0.263381 |
3 | 0.294505 |
4 | 0.262150 |
- ActiveModelSerializerを使うとネストが深くならないので、保守性は高まりそう
- 今回のデータ量だと、若干速度が遅くなることが見られました
- とはいっても、一気に100近くのデータを取得するのにSerializer使う使わないの差はおよそ0.02sほど(ネットワークは加味せず)なので、以前触れたuseSWRを使ったキャッシュに保存やページネーションを使うと特にパフォーマンスに問題はない範囲と感じた
終わりに
AMSを使ってみたと同時に処理時間も調べてみました。
コードは綺麗になっていいです。
今回の検証結果の差程度であれば、コードのみやすさからAMSは導入した方がいいと思いました。
ただ、AMSは複雑なデータ構造だとパフォーマンスが落ちるようです。
AMSよりパフォーマンスがいいjson-api-serializerやJBの使用検討も今後行う必要があると思いました。
未熟な文ですが、ここまでみていただきありがとうございます。
ご指摘いただける点があれば、ぜひよろしくお願いします。