虎の穴開発室ブログ

虎の穴ラボ株式会社所属のエンジニアが書く技術ブログです

MENU

RailsのJSONシリアライザをActiveModelSerializersからjbに変更してみた

こんにちは。虎の穴ラボのH.Kです。
ちょっと前まではJavaを書いていましたが、最近はRuby on Railsでアプリケーションの開発を行っています。
Railsアプリケーションの性能改善の一環として、JSONシリアライザを「ActiveModelSerializers」から「jb」に切り替えました。
今回は、その手法をお伝えします。

それぞれのシリアライザについて

ActiveModelSerializers

github.com
言わずと知れた(?)Railsのシリアライザで Jbuilderとともによく使われています。

複雑なJSONであるほど真価を発揮するような構造になっています。
これはベースとしてテンプレートを返却する思想のJbuilderに対して、クラスとして定義し、定義に従いシリアライズするという思想があるためです。

jb

github.com

先述のJbuilderを拡張して作成されています。
特徴として、上記のGitのReadmeに以下のことが挙げられています。

・No ugly builder syntax
・No method_missing calls
・render_partial with :collection option actually renders the collection (unlike Jbuilder)

つまりJbuilderの使いにくかった部分を改良したGemとなっています。
今回の変更理由ですが、jbが非常に高速であることが挙げられます。
余談ですが、私の中でJbuilderといえばJavaのIDEのほうが先に浮かびます。

導入

Gitのほうにも書いてあるとおり、他のGemと同じようにGemfileに書いてbundle installで導入できます。

gem 'jb'

書き換えのポイント

実際にどのように書き換えたのか例示を交えて解説します。

継承関係

ActiveModelSerializersでは継承関係を使って、プロパティを引き継いでいくことができます。
例えば以下のような形です。
user_serializer.rb

class UserSerializer <  ActiveModel::Serializer
  attributes :id,
             :name
end

user_detail_serializer.rb

class UserDetailSerializer <  UserSerializer
  attributes :age
end

UserDetailSerializerを使いシリアライズすると以下のようなJSONが生成できます。

{
  "id": 1,
  "name": "h.k",
  "age": 28
}

これをjbで実現するためには2つの方法が考えられます。

  1. 継承関係を意識して移行する
  2. 一つのテンプレートにまとめてしまう

今回は1の方法で実施します。なお、実行速度だけを考えれば2を取るべきかと思います。

user_serializer.json.jb

res = {
  id: user.id,
  name: user.name
}
res

user_detail_serializer.json.jb

res = render partial: 'api/user/user_serializer', locals: {user:user} # 先にuser_serializer側のオブジェクトを受け取る
res[:age] = user.age # user_serializerで作成したオブジェクトにプロパティを追加する
res

こうすることによってUserDetailSerializerと同じ形式のJSONを返却することができます。

なお、以下のようにhash#mergeを使用しても実装できます。見通しはこの方が良いですが、mergeメソッドがあまり高速ではないためおすすめしません。

res = render partial: 'api/user/user_serializer', locals: {user:user} # 先にuser_serializer側のオブジェクトを受け取る
tmp = {
  age: user.age
}
res.merge(tmp)

メソッド化されたプロパティ

先程のUserDetailSerializerを修正して例示します。

class UserDetailSerializer <  UserSerializer
  attributes :age,
             :display_adult_items

  def display_adult_items
    object.age >= 18 # シリアライズする対象のモデルがobjectに入っている
  end
end

このように導出した結果をJSONで返却したい場合、ActiveModelSerializersではメソッド化することで実装できます。

これをjbで実現するためにはワンライナーで実装するか変数として処理する必要があります。
※あくまでテンプレートであり、メソッドを作成することができないためです。
変数として処理をする場合もコントローラ側からlocalsを使って引き渡す方法とテンプレート内で定義する方法があります。
コントローラ側から渡す場合、以下のように実装できます。
UserController.rb

display_adult_items = user.age >= 18 # 先に導出する
render 'api/user/user_detail_serializer', formats: 'json', handlers: 'jb', locals: { user: user, display_adult_items: display_adult_items} # localsで引き渡す

user_detail_serializer.json.jb

res = render partial: 'api/user/user_serializer', locals: {user:user}
res[:age] = user.age
res[:display_adult_items] = display_adult_items # 引き渡されたdisplay_adult_itemsをそのまま設定する
res

この方法の注意点としては、user_detail_serializer.json.jbを他のテンプレートから呼び出そうとした際にdisplay_adult_itemsをlocalsでちゃんと設定しておかないとエラーになってしまう点です。

次にテンプレート内で定義する方法です。といってもそのまま愚直にやるだけです。

user_detail_serializer.json.jb

res = render partial: 'api/user/user_serializer', locals: {user:user}
res[:age] = user.age
display_adult_items = user.age >= 18 # 先に導出する
res[:display_adult_items] = display_adult_items # 設定する
res

リレーション

今回はUserが複数のPost(投稿)を保持するケースで例示します。
まずはActiveModelSerializersです。PostSerializerは作成済みとして読んでください。

user_detail_serializer.rb

class UserDetailSerializer <  UserSerializer
  attributes :age,
             :posts
  has_many :posts, serializer: PostSerializer
end

これだけで複数Postのシリアライズまでできます。
次にこれをjbを使ったシリアライズに書き換えます。

user_detail_serializer.json.jb

res = render partial: 'api/user/user_serializer', locals: {user:user} 
res[:age] = user.age 
res[:posts] = render partial: 'api/posts/post_serializer', collection: user.posts, as: :post
res

collectionで繰り返す対象を指定して、as: :postで変数名を設定します。
これにより一つのpostをシリアライズするapi/posts/post_serializerで複数の投稿を扱えます。

つまったポイント

ここからはjbに書き換える上で少し詰まったところと解決方法を示します。

空の文字列が返ってしまう

ちゃんとissueに解決方法が書いてありました。

github.com

結論としてはApplicationControllerActionView::Renderingが必要でした。
もともとactive_model_serializersで使っていたControllerをそのまま書き換えた結果、ActionView::Renderingを設定しておらず気づけませんでした。

class Api::UsersController < Api::ApplicationController
  include ActionView::Rendering # 追加しないとテンプレートを返せない
end

Helperは使える?

こちらはJBuilderのissueに書いてありました。

github.com

ApplicationControllerを継承したクラスにhelper ApplicationHelperを追加するだけで、他のviewsと同じようにHelperが使用できます。

class Api::UsersController < Api::ApplicationController
  helper ApplicationHelper # 追加するとヘルパーが使える
  include ActionView::Rendering
end

別件:Windows×Rails×RuboCop

このブログ書くために私用のWindows機にRails環境を作っていたのですが、RuboCop入れたら以下の警告がたくさん出て大変でした。
Layout/EndOfLine: Carriage return character missing.
何が起きていたかというと、rails new プロジェクト名で作成された各ファイルの改行コードがLFで設定されていました。
RubocopのLayout/EndOfLineの警告を確認するとWindowsではReturn character is CR+LF on Windows.とあります。

www.rubydoc.info

これを解消するためにすべてのファイルの改行コードをCR+LFに変換するのが地味に大変でした。
最近ではWSL2も便利みたいなので、開発環境をそちらに移すことも検討したいところです……。

まとめ

今回のシリアライザの書き換えで最大で25%ほどの速度改善ができました。
まだいくつかのAPIしか切り替えていませんが、ほとんど変わらなかったところもあります。
今後もなにか良い改善ができれば、ブログにて発信をしていきたいと思います。

P.S

【Amazonギフト券3,000円分がもらえる!?】虎の穴ラボ公式Twitterリツイートキャンペーン!

現在、虎の穴ラボ公式Twitterにて「エンジニアの働き方調査!」と題してリツイートキャンペーンを開催しています。

■キャンペーン概要

エンジニアにとってより良い環境を整える事を目的に、皆様の考える今後の働き方と現状についてお考えを知りたいと思い、虎の穴ラボ公式Twitter(@toranoana_lab)にてアンケート調査を行っています!
虎の穴ラボ公式Twitterをフォロー&RTの上、アンケートにご回答くださった方を対象に、Amazonギフト券3,000円分を抽選で20名様にプレゼント!
アンケートの回答期限は8/20(木)23:59までとなりますので、皆様のお考えを聞かせて頂けると嬉しいです。
🔻「エンジニアの働き方調査!」ツイートはこちら

虎の穴ラボ主催LT会開催!

虎の穴ラボ主催のオンラインLTイベントを 8/26(水)19:30〜 開催します!
今回もフリーテーマとなっており、ITに関連する内容であれば、何でも大歓迎ですので、初心者の方も練習の場としてお気軽にご参加ください!
connpassにて参加受付中です!

yumenosora.connpass.com

その他採用情報

虎の穴ラボでの開発に少しでも興味を持っていただけた方は、採用説明会やカジュアル面談という場でもっと深くお話しすることもできます。ぜひお気軽に申し込みいただければ幸いです。
カジュアル面談では虎の穴ラボのエンジニアが、開発プロセスの内容であったり、「今期何見ました?」といったオタクトークから業務の話まで何でもお応えします。

カジュアル面談や採用情報はこちらをご確認ください。
yumenosora.co.jp