はじめに
ここ半年ぐらいActiveModel::Serializers
を使った開発をしていて
痒い所に手が届く書き方をいくつか見つけたので紹介していきます
基本的な使い方はすでにいろんな方の記事があるので省略します
1. キー名を変えたい
id
first_name
last_name
employee_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_to
has_one
has_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
してます