メドピア開発者ブログ

集合知により医療を再発明しようと邁進しているヘルステックリーディングカンパニーのエンジニアブログです。読者に有用な情報発信ができるよう心がけたいので応援のほどよろしくお願いします。

ActiveModelSerializersを使った所感

こんにちは。メドピアエンジニアの保立です。

メドピアでは、ドクター向けに運営している「MedPeer」のiOSアプリを3月15日にリリース致しました!!!
今回は、iOSアプリ開発の過程で、APIの実装にActiveModelSerializersを使ったので、そこで得た知見を書きます。

ActiveModelSerializers を使った理由

json形式のレスポンスを返却する場合、jbuilderを使うケースも多いのではないでしょうか。
メドピアでも、今まで外部へのAPIにはjbuilderを使用していました。
しかし、iOSアプリ用のAPIでは、以下のメリットを考慮して、ActiveModelSerializersを使うことにしました。

メリット① 複雑なjsonを返す際に、ActiveModelSerializersの方が、レスポンスが早い

スマートフォン用のAPIでは、なるべく少ないリクエストで画面の描画に必要なすべての情報を返却したいため、何重にも入れ子になるjsonを返却することが多いと思います。そのため、viewをレンダリングするjbuilderでは汎用的なオブジェクトはpartial化することが求められますが、partialを呼び出すのに時間がかかってしまいます。
一方、ActiveModelSerializersでは、レスポンスの内容を定義するserializerにhas_manyhas_oneを使って、関連するオブジェクトを指定することができます。これにより、jbuilderよりも素早くレスポンスを返すことができます。

メリット② DSLな書き方をしなくてよい

jbuilderの書き方は、直感的に分かりづらい記法になるケースがあります。

json.title  @post.title
json.body @post.body
# {"title": "タイトル", "body": "本文"}


json.post @post, :title, :body
# {"post": {"title": "タイトル", "body": "本文"}}}

# これも同じ内容を返す(こっちの方が少し分かりやすい)
json.post do
  json.title @article.title
  json.body @article.body
end

さらに、partialを読んだり、if文などの分岐が入ると、より分かりづらくなります。
ActiveModelSerializersでは、Rubyの記法に則って書けるため、分かりやすいコードになります。

以上の点から、メドピアのiOSアプリ用APIとして、ActiveModelSerializersを採用しました。

使用例

ActiveModelSerializerは以下のように使用します。

class PostsController < ApplicationController
  def show
    @post = Post.find(params[:id])
    render json: @post, serializer: PostSerializer
  end
end


class PostSerializer < ActiveModel::Serializer
  attribute :title

  # 別名をつけたい時はkeyを使用する
  attribute :created_at, key: :timestamp

  # has_manyやhas_oneで関連するオブジェクトを指定する
  has_many :comments,  serializer: CommentSerializer

  # メソッドを呼び出すことも可能
  attribute :published

  def published
    object.published_at.present?
  end  
end

class CommentSerializer < ActiveModel::Serializer
  attribute :body
end

# レスポンス
{  
  title: "タイトル",
  timestamp: "2018-01-01T00:00:00+09:00",
  published: true,
  comments: [
    {
       body: "コメント" 
    }  
  ]
}



並列関係の複数Modelに紐づくAPIを返却したい場合もあります。 以下の例は、アプリ起動時など、Masterデータを取得する時のコードです。

class MasterDataController
  render json: MasterData.new, serializer: MasterDataSerializer
end


class MasterData
  def first_master_data
    @first_master_data ||= Master::FirstMasterData.all
  end

  def second_master_data
    @second_master_data ||= Master::SecondMasterData.all
  end
end


class MasterDataSerializer
  has_many :first_master_data
  has_many :second_master_data

  class FirstMasterDataSerializer
    attributes :id, :name
  end

  class SecondMasterDataSerializer
    attributes :id, :name
  end
end

使ってみた感想

書いていて、ものすごくつまずくということがなかったです。
ActiveRecordの延長のような作りなので、つまずくことなく書くことができました。
またGitHubのドキュメントが豊富なので、基本的にわからないことがあってもドキュメントをみれば解決しました。
コード量はjbuilderより多くなるので、「すぐに実装したい」「複雑なjsonを返す必要がない」場合はjbuilderでもいいと思います。 逆に、長い間運用したり、拡張する可能性があるAPIを用意する場合は、ActiveModelSerializerおすすめです^^

おまけ(Active Model Serializer と jbuilder のパフォーマンス比較)

posts#showアクションにて、postsを1件とhas_many関係のcommentsを20件をレスポンスに設定するケースで比較します。

jbuilder

# app/controllers/posts_controller.rb
class PostsController < ApplicationController
  def show
    @post = Post.find(params[:id]).preload(:comments)
  end
end

# app/views/posts/show.json.jbuilder
json.title  @post.title
json.page_views do
  json.partial! partial: 'comment', collection: @post.comments, as: :comment
end

# app/views/posts/_comments.json.jbuilder
json.body comments.body

実行結果は以下のようになります。

Processing by PostsController#show as JSON
Posts Load (1.0ms)  SELECT `posts`.* FROM `posts` LEFT JOIN `comments` ON `posts`.`id` = `comments`.`post_id` 
Rendered posts/_comments.jbuilder (0.3ms)
Rendered posts/_comments.jbuilder (0.3ms)
Rendered posts/_comments.jbuilder (0.3ms)
(commentsの数だけ続く)
Rendered posts/_comments.jbuilder (0.3ms)
Rendered posts/show.json.jbuilder (47.2ms)
Completed 200 OK in 87ms (Views: 49.6ms | ActiveRecord: 7.5ms)

Active Model Serializers

# app/controllers/posts_controller.rb
class PostsController < ApplicationController
  def show
    @post = Post.find(params[:id]).preload(:comments)
    render json: @post, serializer: PostSerializer
  end
end

# app/serializers/posts_serializer.rb
class PostSerializer < ActiveModel::Serializer
  attribute :title
  # has_manyやhas_oneで関連するオブジェクトを指定する
  has_many :comments,  serializer: CommentSerializer
end

class CommentSerializer < ActiveModel::Serializer
  attribute :body
end

実行結果は以下のようになります。

Processing by PostsController#show as HTML
Posts Load (1.0ms)  SELECT `posts`.* FROM `posts` LEFT JOIN `comments` ON `posts`.`id` = `comments`.`post_id` 
[active_model_serializers] Rendered PostSerializer with ActiveModelSerializers::Adapter::Attributes (11.23ms)
Completed 200 OK in 41ms (Views: 9.7ms | ActiveRecord: 7.9ms)

と、いうことで2倍以上の速度でレスポンスを返すことができました。
実際のMedPeerアプリでは、3重4重に入れ子にしたjsonを返却することがあるため、Active Model Serializers の恩恵は計り知れないものがあります。


(☝︎ ՞ਊ ՞)☝︎是非読者になってください


メドピアでは一緒に働く仲間を募集しています。 ご応募をお待ちしております!

■募集ポジションはこちら

https://medpeer.co.jp/recruit/entry/

■開発環境はこちら

https://medpeer.co.jp/recruit/workplace/development.html