0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ActiveModelSerializerを触ってみた

Posted at

はじめに

ActiveModelSerializerを業務で使ったのでざっくりと使い方をまとめようと思います。
誤り等ございましたら、コメントいただけるとありがたいです。

セットアップ

インストール方法

  • Gemのインストール手順
    gem 'active_model_serializers', '~> 0.10.0'
    
    出力されるjson構造を変更したい場合(defaultはattributes),
    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ファイルの内容

seed.rb

# 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."

Screenshot 2024-09-25 at 10.53.55.png
元からあるUserとTweetの数を足した結果がUser数: 8, Tweet数: 103.
これで速度測定を行います。

実験方法

rubyにdefalutであるbenchmarkを使ってレスポンス速度を測定します

  1. Serializerなし,データ全渡し
  2. Serializerなし,データ絞り込み渡し
  3. 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
    

Screenshot 2024-09-25 at 13.33.58.png

回数 時間 (秒)
初回 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

Screenshot 2024-09-25 at 13.36.53.png

回数 時間 (秒)
初回 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

Screenshot 2024-09-25 at 13.54.30.png

回数 時間 (秒)
初回 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の使用検討も今後行う必要があると思いました。

未熟な文ですが、ここまでみていただきありがとうございます。
ご指摘いただける点があれば、ぜひよろしくお願いします。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?