ActiveModelSerializers(0.10系)のインスタンス生成時に引数を渡してSerializerクラス内で使う方法

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/110571/6faab0bd-a73a-b130-bcbc-7e670803a01c.png)
> 画像は『[Rails: ActiveModelSerializersでAPIを作る](https://techracho.bpsinc.jp/hachi8833/2017_09_28/45536)』より引用

# TL; DR
1. `ActiveModelSerializers`インスタンス生成時に、`key: value`で引数を渡すことができる
2. `ActiveModelSerializers`クラス側で、`@instance_options`のハッシュから値を取り出すことができる

# 動作環境
- Ruby：2.6.1
- Rails：5.2.3
- ActiveModelSerializers：0.10.10

> ActiveModelSerializersが0.10であれば他はそこまで気にしなても良いです

# 前提
## 簡易コード
以下が、今回説明するために使う簡易的なコードです。

### ArticlesController
```rb
module Api
  module V1
    class ArticlesController < Api::ApplicationController
      def index
        articles = Article.order(created_at: :desc).page(params[:page])
        serializable_resource = ActiveModelSerializers::SerializableResource.new(
          articles,
          includes: '**',
          each_serializer: ArticleSerializer
        )
        json = { articles: serializable_resource.as_json }
        render json: json
      end
    end
  end
end
```

### ArticleSerializer
```rb
class ArticleSerializer < ActiveModel::Serializer
  attributes :id, :title, :content, :image_url
end
```

## やりたいこと
ArticleSerializer内で、現在ログインしているユーザーを表す`current_user`のデータを使いたい場面があるとします。そのため、ArticleControllerから`current_user`の情報を渡してあげる必要があります。

今回は「記事をユーザーが読んだこと（=既読）」を表す中間テーブル`UserArticle`の有無を確認するために、`current_user`を引数に渡す必要があるとします。

## 少し調べたけどできなかったこと
- `scope`や`optioins`ハッシュを用いたら取り出せそう...で、できなかった
- おそらくActiveModelSerializersのバージョンが異なるためだと思われる

[![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/110571/5e4793b3-ca73-aeb2-56b8-36722b6755ef.png)](https://qiita.com/kakkunpakkun/items/1c23b936d13f08a42752#%E3%81%AA%E3%81%AB%E3%81%8B%E5%80%A4%E3%82%92serializer%E3%81%AB%E6%B8%A1%E3%81%97%E3%81%9F%E3%81%84)
[![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/110571/5c9ccdd0-d7ed-4419-ef3c-a5f7499d87d0.png)](https://qiita.com/romukey/items/b78906e91a0355e1e77a#%E6%9C%AC%E9%A1%8C-each_serializer%E3%81%AE%E3%82%AA%E3%83%97%E3%82%B7%E3%83%A7%E3%83%B3%E3%81%A7%E5%91%BC%E3%81%B3%E5%87%BA%E3%81%97%E5%81%B4%E3%81%8B%E3%82%89%E5%80%A4%E3%82%92%E5%85%A5%E3%82%8C%E3%81%9F%E3%81%84%E3%81%A8%E3%81%8D)

> 調査している時に参考にさせていただいた記事。おかげさまで、ハッシュを使うんだろうなーというあたりはつきました。

# 解決法
## `@instance_options`でおｋ
`bingind.pry`したらすぐに分かったのですが、**`@instance_options`内に引数のデータがハッシュ形式で格納されている**ことが判明しました。なので、このオプションから引数のハッシュを取り出せばやりたいことが実現できます。

```sh
$ rails c
$ serializable_resource
=> #<ActiveModelSerializers::SerializableResource:0x000055e581f4abe8
 @adapter=
  #<ActiveModelSerializers::Adapter::Attributes:0x00007fd6c4089b78
   @instance_options={:include=>"**", :fieldset=>#<ActiveModel::Serializer::Fieldset:0x00007fd6c4089ab0 @fields={}, @raw_fields={}>},
   @serializer=
    #<ArticleSerializer:0x000055e57db81ce0
     @attributes=
      {:id=>1,
       :title=>"test",
       :content=>"test",
       :image=>nil
   ~<中略>~
     # ココ
     @instance_options=
      {:current_user=>
        #<User:0x000055e57c1d5b20
         id: 1,
```

ActiveModelSerializersのソースを見ると、以下の部分が該当します（最初から見ておけばよかった...）

```rb
attr_reader :serializer, :instance_options

def initialize(serializer, options = {})
  @serializer = serializer
  @instance_options = options
end
```

https://github.com/rails-api/active_model_serializers/blob/0-10-stable/lib/active_model_serializers/adapter/base.rb#L51

## 簡易コード（追記）
先ほどのコードに上記を反映すると、以下のようになります。

### ArticlesController
```rb
module Api
  module V1
    class ArticlesController < Api::ApplicationController
      def index
        articles = Article.order(created_at: :desc).page(params[:page])
        serializable_resource = ActiveModelSerializers::SerializableResource.new(
          articles,
          includes: '**',
          each_serializer: ArticleSerializer,
          current_user: current_user
        )
        json = { articles: serializable_resource.as_json }
        render json: json
      end
    end
  end
end
```

### ArticleSerializer
```rb
class ArticleSerializer < ActiveModel::Serializer
  attributes :id, :title, :content, :image_url, :is_read

  def is_read
    current_user = @instance_options[:current_user]
    UserArticle.where(user: current_user, article: object).exists?
  end
end
```

# 小ネタ（おまけ）
## 引数を用いた条件分岐
引数を用いてSerializerで作るJSONを出し分けたい時やnilガードを仕込みたい時などは、以下のように**引数を用いた条件分岐**を実装することも可能です。

JSONのキーを出し分ける場合は、`attributes`と別に`attribute`を定義する必要があるので注意です。

### ArticleSerializer
```rb
class ArticleSerializer < ActiveModel::Serializer
  attributes :id, :title, :content, :image_url

  attribute :is_read, if: :current_user?

  def current_user?
    @instance_options.key?(:current_user)
  end

  def is_read
    return false if @instance_options[:user_articles].nil?
    UserArticle.where(user: current_user, article: object).exists?
  end
end
```

## privateメソッドは使えない
内部でしか使わないメソッドを以下のようにprivateメソッドに閉じ込めたいのですが、そのようにすると**エラー**が返ってきます。

### ArticleSerializer
```rb
class ArticleSerializer < ActiveModel::Serializer
  attributes :id, :title, :content, :image_url

  attribute :is_read, if: :current_user?

  def is_read
    UserArticle.where(user: current_user, article: object).exists?
  end

  private

  # こういう感じにはできない
  def current_user?
    return false if @instance_options[:user_articles].nil?
    @instance_options.key?(:current_user)
  end

end
```

### エラー文
```sh
NoMethodError: private method `current_user?' called for #<ArticleSerializer:0x000055729750f848>
from /usr/local/bundle/gems/active_model_serializers-0.10.10/lib/active_model/serializer/field.rb:62:in `public_send'
```

## N+1回避
クラスの設計にもよりますが、先ほどのようにSerializerの中でモデルに対し`where`や`exists?`を使ってDBに問い合わせを行うと[N+1](https://qiita.com/massaaaaan/items/4eb770f20e636f7a1361)が発生する原因になり得ます（実は、先ほどのSerializerの使い方でもN+1が発生します）。

その場合は、**Controller側で先にDBへの問い合わせをまとめて行い、その結果を引数に渡す**などの工夫をすれば良いでしょう。

### ArticleController
```rb
module Api
  module V1
    class ArticlesController < Api::ApplicationController
      def index
        articles = Article.order(created_at: :desc).page(params[:page])
        # ここで問い合わせを行い、先にハッシュを作っておく
        user_articles = current_user.user_articles.where(article_id: articles.map(&:id)).pluck(:article_id)
        serializable_resource = ActiveModelSerializers::SerializableResource.new(
          articles,
          includes: '**',
          each_serializer: ArticleSerializer,
          user_articles: user_articles # 先ほど作ったハッシュを引数に渡す
        )
        json = { articles: serializable_resource.as_json }
        render json: json
      end
    end
  end
end
```

### ArticleSerializer
```rb
class ArticleSerializer < ActiveModel::Serializer
  attributes :id, :title, :content, :image_url

  attribute :is_read, if: :user_articles?

  def user_articles?
    @instance_options.key?(:user_articles)
  end

  def is_read
    return false if @instance_options[:user_articles].nil?
    # DBではなく引数で指定したハッシュに対し問い合わせを行う
    @instance_options[:user_articles].include?(object.id)
  end
end
```