はじめに
ここ半年ぐらいActiveModel::Serializersを使った開発をしていて
痒い所に手が届く書き方をいくつか見つけたので紹介していきます
基本的な使い方はすでにいろんな方の記事があるので省略します
1. キー名を変えたい
idfirst_namelast_nameemployee_number
といったカラムを持つUserモデルがあったときに
first_nameをnameとして出力したい場合
a. :keyオプションを使う
class UserSerializer < ActiveModel::Serializer
attributes :id, :employee_number
attribute :first_name, key: :name
end
b. メソッドを定義する
class UserSerializer < ActiveModel::Serializer
attributes :id, :name, :employee_number
def name
object.first_name
end
end
c. ブロックで渡す
class UserSerializer < ActiveModel::Serializer
attributes :id, :name, :employee_number
attribute :name do
object.first_name
end
end
※objectはSerializernewなどで渡されたオブジェクト(レコード)自身
-
attributeだけでなくbelongs_tohas_onehas_manyでも同様の使い方が可能 - メソッドやブロックは処理も書けるので分岐や加工がしやすくて便利
2. has_manyで紐づくモデルをSerializerの中でorderするとSQLキャッシュのN+1が起きる問題をなんとかしたい
class User < ActiveRecord::Base
has_many :posts
end
class Post < ActiveRecord::Base
belongs_to :user
end
class UserSerializer < ActiveModel::Serializer
attributes :id, :name, :employee_number
has_many :posts, serializer: PostSerializer do
object.posts.order(created_at: :desc)
end
end
と書くと事前にpreloadなどをしてN+1対策をしていても
CollectionSerializerで大量に出力する際にSQLキャッシュのN+1が発生します
ログの中にCACHE (0.0ms) SELECT ....みたいなのが大量に出て
キャッシュされているとはいえやはり遅くなる
そんな時の対策はこちら
a. モデルのアソシエーションにデフォルトのオーダーを仕込む
class User < ActiveRecord::Base
has_many :posts, -> { order(created_at: :desc) }
end
class UserSerializer < ActiveModel::Serializer
attributes :id, :name, :employee_number
has_many :posts, serializer: PostSerializer
end
としてやると最初にオーダーされてキャッシュのN+1が発生しません
ただ、これだと他のところでこのorderが効いて欲しくない時があるかも
そんな時はモデルにもう一個別のアソシエーションを定義します
b. モデルにオーダー用のアソシエーションを定義する
class User < ActiveRecord::Base
has_many :posts
has_many :for_order_posts, -> { order(created_at: :desc) }, class_name: :Post
end
class UserSerializer < ActiveModel::Serializer
attributes :id, :name, :employee_number
has_many :for_order_posts, serializer: PostSerializer
end
もちろんpreloadでのアソシエーションも:for_order_postsに直します
個人的にはこれが好きです
3. has_manyのアソシーションの最新1件を含めたいけどこれまたSQLキャッシュのN+1が起きる問題
class User < ActiveRecord::Base
has_many :posts
end
class UserSerializer < ActiveModel::Serializer
attributes :id, :name, :employee_number
attribute :newest_post, serializer: PostSerializer do
object.posts.order(updated_at: :desc).limit(1)
end
end
ってやるとやっぱりSQLキャッシュのN+1が発生するので
やっぱりここも別のアソシエーションを定義します
a. has_oneとscopeをモデル側に作る
class User < ActiveRecord::Base
has_many :posts
has_one :newest_post, -> { order(updated_at: :desc) }, class_name: :Post
end
class UserSerializer < ActiveModel::Serializer
attributes :id, :name, :employee_number
attribute :newest_post, serializer: PostSerializer
end
has_oneなので勝手にlimit(1)がかかります
4. attributeで出力するか否かの条件をつけたい
a. :ifオプションを使う
class UserSerializer < ActiveModel::Serializer
attributes :id, :name, :employee_number
attribute :name, if: -> { object.has_attribute?(:name) }
end
ってやるとnameカラムが存在するときだけ出力してくれたり
ポリモーフィックで
UserモデルがChildBoyとChildGirlというモデルをChildという名前で持っていた時に
ChildBoyの時はChildGirlを出さない、みたいな使い方もできます
5. おまけ(入れ子になったSerializerのテストコード)
当たり前かもしれませんが自分はいつも入れ子になったSerializerのrspecを書く時はいつもこうやってます
it 'serializes User' do
expect(UserSerializer.new(user).to_json).to be_json_including(
id: user.id,
first_name: user.first_name,
last_name: user.last_name,
posts: user.posts.map do |post|
JSON.parse(PostSerializer.new(post).to_json)
end
)
end
もちろんPostSerializerがテストされている前提です
PostSerializer.new(post).to_hashでもいいんですが
jsonに変換した時に日時のフォーマットが変わっちゃったりするのでJSON.parseしてます