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はSerializersとAdaptersという2つのコンポーネントからなります。
- Serializersは、どの属性をAPIに出力するかを記述します。
- Adaptersは、どのようなフォーマットでAPIを出力するかを記述します。
メインで書く部分はSerializersになります。Adaptersは今のところ、フォーマットの選択だけです。
サンプル
それでは実際に書いて動かしてみましょう。いつもサンプルコードのままでは面白く無いので、今回はtwitterのAPIを想定して作ってみます。と言っても構成は大体同じですw
作るもの
GET /users
で全てのUserの情報と、そのUserがTweetした全てのTweetをAPIで返します。
完成したソースコードはこちらにあります。
動作環境
- 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_many
やhas_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
を指定するだけです。またページネーションについてはkaminari
かwill-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になっているのではないでしょうか。