LoginSignup
37
20

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-12-29

image.png

画像は『Rails: ActiveModelSerializersでAPIを作る』より引用

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

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

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

やりたいこと

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

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

少し調べたけどできなかったこと

  • scopeoptioinsハッシュを用いたら取り出せそう...で、できなかった
  • おそらくActiveModelSerializersのバージョンが異なるためだと思われる

image.png
image.png

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

解決法

@instance_optionsでおk

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

$ 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のソースを見ると、以下の部分が該当します(最初から見ておけばよかった...)

attr_reader :serializer, :instance_options

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

簡易コード(追記)

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

ArticlesController

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

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

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

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

エラー文

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の中でモデルに対しwhereexists?を使ってDBに問い合わせを行うとN+1が発生する原因になり得ます(実は、先ほどのSerializerの使い方でもN+1が発生します)。

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

ArticleController

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

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
37
20
1

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
37
20