RESTful Hypermedia API サーバサイド編 – active_model_serializers

モバイルアプリサービス部の五十嵐です。

サーバサイド編の2回目は、active_model_serializersを紹介します。Rails5のRails APIにも使われる予定の注目のGemです。最新のバージョンの0.10.0ではデータアダプターとしてJSON APIをサポートしていますので、その辺も試してみたいと思います。

active_model_serializersとは

active_model_serializers(以下、AMS)は、Railsの基本思想である『設定より規約』のもと、JSONを生成します。AMSはSerializersAdaptersという2つのコンポーネントからなります。

  • Serializersは、どの属性をAPIに出力するかを記述します。
  • Adaptersは、どのようなフォーマットでAPIを出力するかを記述します。

メインで書く部分はSerializersになります。Adaptersは今のところ、フォーマットの選択だけです。

サンプル

それでは実際に書いて動かしてみましょう。いつもサンプルコードのままでは面白く無いので、今回はtwitterのAPIを想定して作ってみます。と言っても構成は大体同じですw

作るもの

GET /users で全てのUserの情報と、そのUserがTweetした全てのTweetをAPIで返します。

完成したソースコードはこちらにあります。

bisque33/ams-sample

動作環境

  • ruby (2.2.3)
  • rails (4.2.4)
  • active_model_serializers (0.10.0.rc3)

Installation

まずアプリケーションを作成します。

$ rails new twitter-api --skip-bundle --skip-test-unit -q && cd twitter-api

次にGemfileに必要なgemを設定していきます。ページネーションも実装するので、kaminariも入れておきます。

# Gemfile
gem 'active_model_serializers', '0.10.0.rc3'
gem 'kaminari'

設定したらbundle installを行います。

$ bundle install

Creating a Serializer

AMSでは、serializerというレイヤーが提供されており、そこにAPIで出力する属性やモデルのアソシエーションを定義します。すごく簡単に言うならAPIのViewに相当するものと考えれば良いと思います。

またSerializerにはgeneraterも提供されており、rails g serializerコマンドによりSerializerを生成できますし、rails g resourceコマンドを使えばController、Model、Serializerの全てが(もちろんspecも)一度に生成できます。

今回はrails g resourceコマンドを使います。

$ bundle exec rails g resource user name:string profile:text
$ bundle exec rails g resource tweet body:string user:references

attributesに設定した項目は、JSONに出力される項目になります。アソシエーションはmodelと同様にhas_manyhas_one belongs_toなどが設定できます。userには複数のtweetがぶらさがるので、has_many :tweetsを追加します。

# app/serializers/user_serializer.rb
class UserSerializer < ActiveModel::Serializer
attributes :id, :name, :profile
has_many :tweets
end

またtweetの方はbelongs_toを設定します。ちなみにSerializerのアソシエーションはあくまでSerializerの設定でしかないので、ModelとしてのアソシエーションはModelに記述する必要があることを忘れないで下さい。

# app/serializers/tweet_serializer.rb
class TweetSerializer < ActiveModel::Serializer
attributes :id, :body
attribute :created_at, key: :published_at
belongs_to :user
end

attributeはキーを変更したり、属性そのものを独自に定義することが可能です。また、attributesやアソシエーションは、メソッドを上書きすることにより、独自の定義にすることができます。

Model

次にModelの記述をします。今回はAssociationの設定だけです。

# app/models/user.rb
class User < ActiveRecord::Base
has_many :tweets
end
# app/models/tweet.rb
class Tweet < ActiveRecord::Base
belongs_to :user
end

Controller

次にControllerの記述をします。Controllerは普段通りの書き方で、render :jsonを指定するだけです。またページネーションについてはkaminariwill-paginateが対応しており、出力するAPIにページネーションの属性を追加してくれます。includeはAdapterがJSON APIのときに、アソシエーションのデータも出力したい場合に必要になります。

# app/controllers/users_controller.rb
class UsersController < ApplicationController def index @users = User.all.page(1).per(1) render json: @users, include: ['tweets'] end end ``` ## Adapter Adapterの設定をすることで簡単にJSON APIのフォーマットに対応することができます。 その前にDBをセットアップしてテストデータを投入します。 ```bash $ bundle exec rake db:setup db:migrate $ bundle exec rails c > user = User.create(name: "alice", profile: "7 years old.")
> user.tweets.create(body: "good morning")
> user.tweets.create(body: "good evening")
> user.tweets.create(body: "good night")
> User.create(name: 'bob')
> User.create(name: 'sam')

準備ができたので、まずは標準のJSON Adapterで出力結果を見てましょう。

$ bundle exec rails c
(別のコンソールから)
$ curl -s http://localhost:3000/users | jq .
[
{
"id": 1,
"name": "alice",
"profile": "7 years old.",
"tweets": [
{
"id": 1,
"body": "good morning",
"published_at": "2015-10-01T13:49:26.645Z"
},
{
"id": 2,
"body": "good evening",
"published_at": "2015-10-01T13:49:51.468Z"
},
{
"id": 3,
"body": "good ngiht",
"published_at": "2015-10-01T13:49:55.586Z"
}
]
}
]

ActiveRecordのデータ構造がそのまま出力されたような感じになりました。JSON Adapterの場合はページングの定義がないため、独自にSerializerを定義する必要があります。

次にAdapterにJSON APIを設定して出力してみます。

# config/initializers/active_model_serializer.rb
ActiveModel::Serializer.config.adapter = :json_api

railsを再起動します。

$ bundle exec rails c
(別のコンソールから)
$ curl -s http://localhost:3000/users | jq .
{
"data": [
{
"id": "1",
"type": "users",
"attributes": {
"name": "alice",
"profile": "7 years old."
},
"relationships": {
"tweets": {
"data": [
{
"id": "1",
"type": "tweets"
},
{
"id": "2",
"type": "tweets"
},
{
"id": "3",
"type": "tweets"
}
]
}
}
}
],
"included": [
{
"id": "1",
"type": "tweets",
"attributes": {
"body": "good morning",
"published_at": "2015-10-01T13:49:26.645Z"
},
"relationships": {
"user": {
"data": {
"id": "1",
"type": "users"
}
}
}
},
{
"id": "2",
"type": "tweets",
"attributes": {
"body": "good evening",
"published_at": "2015-10-01T13:49:51.468Z"
},
"relationships": {
"user": {
"data": {
"id": "1",
"type": "users"
}
}
}
},
{
"id": "3",
"type": "tweets",
"attributes": {
"body": "good ngiht",
"published_at": "2015-10-01T13:49:55.586Z"
},
"relationships": {
"user": {
"data": {
"id": "1",
"type": "users"
}
}
}
}
],
"links": {
"self": "http://localhost:3000/users?page%5Bnumber%5D=1&page%5Bsize%5D=1",
"next": "http://localhost:3000/users?page%5Bnumber%5D=2&page%5Bsize%5D=1",
"last": "http://localhost:3000/users?page%5Bnumber%5D=3&page%5Bsize%5D=1"
}
}

これだけでJSON APIに準拠したフォーマットになりました。

所感

いかがだったでしょうか。今回はご紹介できませんでしたが、SerializerとModelは1:1の関係ではないので、目的に応じてSerializerを組み立てることができます。また、Jbuilderと比較すると、Adaperがあることで誰が作っても同じフォーマットの中に収まるということは大きなメリットですし、Serializerも規約に則って定義をするので、よりRailsらしいRailsのAPIになっているのではないでしょうか。

参考