ActiveModel::Serializersとは
Railsなどで簡単で素早くjsonを作れるgemです。
ActiveModel::Serializers brings convention over configuration to your JSON generation.
実際に試してはないですが、ActiveModelに依存しているだけなのでSinatraなどでも使えるはずです。
なぜActiveModel::Serializersを使うのか
簡単で速いので使ってます。
簡単
Railsではjbuilderを使うことが多いですが、記法が直感的じゃなくて多少憶えにくいかな思ってます。
json.(@message, :content, :author)
=> {content: 'hello', author: 'kakkunpakkun'}
ちょっと変わった文です。
ActiveModel::Serializerならこんな感じです。
class MessageSerializer < ActiveModel::Serializer
attributes :content, :author, :role
def role
object.role.name
end
end
呼び出すのもrenderを使えます
render json: @message
ActiveModel::Serializerを継承して使いたいattributesを指定するのが基本です。
roleのようにattributeをオーバーライドするようなことも出来ます。
普通のRubyのクラスなのでテストも書きやすいです。
実際、HTMLを生成するのにはテンプレートがあるといいと思うんですが、jsonはもっとシンプルだし、テンプレートは必要ないんじゃないかと思っていました。
とはいえ、to_jsonだとモデルをいじらなくてはいけなくなってプレゼンテーション層と密着した感じになってしまいます。
ActiveModel::Serializersはそういうjsonの出力をさくっと簡単に担ってくれます。
速い
partialを使うとjbuilderはものすごく遅くなります。
適当にusersをリストアップする場合で計ってみました。
jbuilderを使った場合
def list_with_jbuilder
@users = User.all
render :list_with_jbuilder
end
json.array! @users.each do |user|
json.partial! 'user', user: user
end
json.id user.id
json.email user.email
json.name user.display_name
だいたいusersのようなコレクションをリストにするときはuserの情報を他のところからも呼び出したいことがあるのでpartial化するのですが、そうするとここではuserを100件出力したところ 280〜350ms ほどかかりました。
userを5件だけに絞ると50〜60msで済むのですが、partialを呼び出す件数が多くなればなるほど遅くなっていきます。
これは恐らくpartialを呼ぶたびに0.1〜0.3msほどかかっているためではないかと思います。
I, [2014-08-31T15:30:38.740573 #65161] INFO -- : Processing by UsersController#list_with_jbuilder as */*
D, [2014-08-31T15:30:38.748207 #65161] DEBUG -- : User Load (1.8ms) SELECT `users`.* FROM `users`
I, [2014-08-31T15:30:38.755020 #65161] INFO -- : Rendered users/_user.jbuilder (0.2ms)
I, [2014-08-31T15:30:38.757443 #65161] INFO -- : Rendered users/_user.jbuilder (0.2ms)
I, [2014-08-31T15:30:38.759521 #65161] INFO -- : Rendered users/_user.jbuilder (0.2ms)
# ↑これがuserの件数分続く
partial使わなければいいんですが、そうすると変更箇所が増えて維持が大変になります。
ActiveModel::Serializersを使った場合
def list_with_serializer
users = User.all
render json: ActiveModel::ArraySerializer.new(
users,
each_serializer: UserSerializer
).to_json
end
class UserSerializer < ActiveModel::Serializer
attributes :id, :email, :name
def name
object.display_name
end
end
jsonの配列を作る書き方は色々ありますが、jbuilderの場合と同様にuserの情報を他のところからも呼び出せるようにUserSerializer
を作っておきました。
同じデータを検索したところ、serializerを使った場合は100件で 40ms〜50ms くらいでした。1/7くらいでしょうか
200件300件と増えれば増えるほど差が開きます。
テストが書きやすい
シリアライズするだけのクラスが出来るのでそのクラスをテストするのもやりやすいです。
ARのModelの中にas_jsonやto_builderを書いていくと、いくつか出力したいjsonのパターンを準備したくなると途端に条件分岐や他のメソッドの呼び出しが増えて煩雑になることがあります。
テストも書きにくくなります。
ActiveModel::Serializerを継承してますが比較的シンプルなクラスなのでテストも普通のクラスについて書くように書けるので楽です。
難点
自分はあまり困ってないですが、場合によっては難点かもなと思うところもあります。
ActiveModelに依存する
名前の通り、ActiveModelに依存しています。
ActiveModel::ModelとActiveModel::Serializationを使います。
僕はActiveModelはActiveModel::Validationsとか便利だし、Rails使おうとPadrino使おうと永続化にはActiveRecord使いたいのでActiveModelに依存しちゃっても全然構わないのですが、ActiveModelを利用したい訳じゃない時には嫌なことはあるかも。
次のバージョンで変更が大きそう
現在0.8.xが安定版なんですが、今度出る0.10.xは色々変わりそうです。
まあこれはどのgemでもあるかなと思いますし、じっくりウォッチするしかないですね。
ドキュメントも変更中のせいか量がすごく減っているので0.8のREADMEを見るようにしています。
困った時
ActiveRecord::Baseではないクラスで使いたい
ActiveModel::ModelとActiveModel::Serializationをincludeしたらserializerに渡して使えます。
こういう感じ
class Post
include ActiveModel::Serialization
include ActiveModel::Model
end
なにか値をserializerに渡したい
こまったらoptionsを渡せば何とかなります。
ActiveModel::ArraySerializer.new(
users,
each_serializer: UserSerializer,
message: message # serializer内でoptions[:message]で呼び出せる
)
ただ、これは今度出る0.10では無くなるかもしれません。
代わりのものが候補に挙がってるようですがこれというのが決まってないようです。
関連するモデルを出力したい
has_manyやhas_oneも使えます(あくまで多重度が1か多かを示すだけなのでbelongs_toはありません)
has_many: :posts
みたいに書けます。
どのserializerを使うかも指定できるので関連したモデルの必要なattributesだけ出すのも簡単です。
has_many: :posts, serializer: ShortPostSerializer
ジェネレータを使いたい
ジェネレーターもあります。
rails g serializer post
というわけで
ActiveModel::Serializerでjson周りの困ってたことがだいたい解決しました。
スピードが速くてモデルと切り離せるのがとても良いです。