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をセットアップしてテストデータを投入します。

$ 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になっているのではないでしょうか。

参考