ActiveModelSerializerを触ってみた

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

# セットアップ

#### インストール方法
- Gemのインストール手順
  ```ruby
  gem 'active_model_serializers', '~> 0.10.0'
  ```
  出力されるjson構造を変更したい場合(defaultはattributes),
  config/initializers/active_model_serializers.ファイルを作成し,
  ```ruby
  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形式で渡すとすると
```ruby
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クラスを定義します
  ```ruby
  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を適用すると
```ruby
  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ファイルの内容
```ruby: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3494258/af1ebdce-a77a-a052-2c96-5d915b41fd43.png)
元からあるUserとTweetの数を足した結果がUser数: 8, Tweet数: 103.
これで速度測定を行います。
## 実験方法
rubyにdefalutであるbenchmarkを使ってレスポンス速度を測定します
1. Serializerなし,データ全渡し
2. Serializerなし,データ絞り込み渡し
3. Serializerあり、データ絞り込み渡し

https://zenn.dev/publiclocaldev/articles/63e0fb1eb098ce

これを参考にさせていただきました。
速度はtotalで比較しました。
1と２で渡すデータを少なくするとレスポンス速度が上がるかの検証を行い、2と3でActiveModelSerializerを使うことによるレスポンス速度が上がるかの検証を行います。

想定場面としては、Twitterの投稿一覧画面を想定場面としています。

## 結果
* Serializerなし,データ全渡し
  ```ruby
  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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3494258/aad673fc-5c9a-4189-58b1-6045ad2d37e2.png)

| 回数 | 時間 (秒) |
|------|--------------------|
| 初回    | 0.318343           |
| リロード    | 0.290193           |
| 2    | 0.267181           |
| 3    | 0.290688           |
| 4    | 0.267231           |



* Serializerなし,データ絞り込み渡し
```ruby
  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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3494258/656f6fcb-dcc8-e659-6193-794b5ce88fa3.png)

| 回数 | 時間 (秒) |
|------|----------------------|
| 初回   | 0.306811             |
| リロード  | 0.278567             |
| 2    | 0.252422             |
| 3    | 0.281951             |
| 4    | 0.276592             |




* Serializerあり、データ絞り込み渡し

```ruby
 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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3494258/26aa5a07-c1b3-a815-1e4f-5f6cec34a5e8.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の使用検討も今後行う必要があると思いました。

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