ActiveModel::Serializersを使ってみた
ActiveModel::Serializers(AMS) という、出力するJSONをいい感じにコントロールできるというライブラリを使ってみた記録。多分、RailsでAPIを作るときなんかによく使われるライブラリ、くらいの認識からスタートした。
https://github.com/rails-api/active_model_serializers
出鼻をくじかれる
「よーし使ってみるか」という気持ちで意気揚々と調べ始めたところ、2つの点で出鼻をくじかれた。
README.mdを読んだ
masterブランチの README.md を見ると、ざっくり次のようなことが書かれていた。
- masterブランチへのPRはcloseやめて、0-10-stableブランチへ頼むよ
- 0.10.xバージョンは巨大なメンテナンスバージョンになっているよ
- 0.10.xバージョンは安定版だけど、積極的なメンテナンスはしていないよ
- バージョン1.0に向けての開発はしているよ
要するに、現在の安定バージョンは 0.10.x であるが積極的なメンテナンスはされていないし、PRも基本的に受け付けていない。今は、色々あって1.0に向けて大規模な改修(開発)をしています...、といった感じ。
fast_jsonapiの登場
NetFlix社から、fast_jsonapiというライブラリを開発したというブログが公開された
https://medium.com/netflix-techblog/fast-json-api-serialization-with-ruby-on-rails-7c06578ad17f
これは ActiveModel::Serializers が提供する多くの機能を受け継ぎつつ、パフォーマンスを改善したライブラリらしい。AMS(ActiveModel::Serializersの略称)と比較して、パフォーマンスは25倍近く向上したと書かれている...。
...といった感じで出鼻をくじかれてしまった。
しかしAMSは有名なライブラリ(のよう)だし、fast_jsonapiはAMSに影響を受けているようなので、まぁやって無駄にはならないだろうと考えて当初の予定通り使ってみることにした。
Getting Started
まずは動かすところまでやってみる。ガイドを参考に進める。
まずは、DBスキーマ、モデル、コントローラを適当に一式用意する。
DBスキーマ
create_table "users", force: :cascade do |t|
t.string "name"
t.integer "age"
t.string "sex"
t.boolean "premium"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["name"], name: "index_users_on_name", unique: true
end
モデル
# app/models/user.rb
class User < ApplicationRecord
end
コントローラ
# app/controllers/users_controller.rb
class UsersController < ApplicationController
def index
render json: User.all
end
def show
render json: User.find_by(id: params[:user_id])
end
end
ルーティング
# config/routes.rb
Rails.application.routes.draw do
get 'users' => 'users#index'
get 'users/:user_id' => 'users#show'
end
ここまでで、サーバを起動してリクエストを送るとJSON形式でuserデータを返してくれる状態になった(※ マイグレーションやテストデータの挿入は省略している)。
>> curl -s localhost:3000/users/1 | jq .
{
"id": 1,
"name": "user-0",
"age": 1,
"sex": "male",
"premium": true,
"created_at": "2018-02-03T01:59:54.243Z",
"updated_at": "2018-02-03T01:59:54.243Z"
}
ここからAMSを使ってみる。まずはシリアライザクラスを作る。
bin/rails g serializers User
これで、app/serializers/user_serializers.rbが作られる。ファイルの中身は次のようになっている。
# app/serializers/user_serializers.rb
class UserSerializer < ActiveModel::Serializer
attributes :id
end
attributes
メソッドに指定した値だけをレスポンスするようになるので、この状態で再びリクエストを送ると次のような結果になる。
>> curl -s localhost:3000/users/1 | jq .
{
"id": 1
}
attributes
メソッドにid
しか指定してないので、レスポンスに含まれるデータもidのみとなった。
attributes
に複数の引数を指定することももちろん可能。
# app/serializers/user_serializers/user_serializers.rb
class UserSerializer < ActiveModel::Serializer
attributes :id, :name, :age
end
レスポンスも合わせて変化する。
>> curl -s localhost:3000/users/1 | jq .
{
"id": 1,
"name": "user-0",
"age": 1
}
ここまでで、とりあえずAMSがどんなものかのイメージができた。
アソシエーションに対応させる
次は、モデルのアソシエーションに対応させてみる。 Userは0個以上のDeckを持てるものとして、データモデルを変更する。
DBスキーマ
create_table "decks", force: :cascade do |t|
t.string "name"
t.string "description"
t.string "image_url"
t.boolean "favorite"
t.integer "user_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["user_id"], name: "index_decks_on_user_id"
end
モデル
# app/models/user.rb
class User < ApplicationRecord
has_many :decks
end
# app/models/deck.rb
class Deck < ApplicationRecord
belongs_to :user
end
シリアライザ
# app/serializers/user_serializer.rb
class UserSerializer < ActiveModel::Serializer
attributes :id, :name, :age
has_many :decks # ★ ここ追加
end
# app/serializers/deck_serializer.rb
class DeckSerializer < ActiveModel::Serializer
attributes :id, :name
end
この状態でリクエストする。すると、次のように関連するモデルのデータもレスポンスにのってくる。
>> curl -s localhost:3000/users/1 | jq .
{
"id": 1,
"name": "user-0",
"age": 1,
"decks": [
{
"id": 1,
"name": "deck-0"
},
{
"id": 11,
"name": "deck-10"
}
]
}
同様に、 has_one
やbelongs_to
というメソッドもある。ActiveRecordの規則に従っているようでわかりやすい。
アダプタを切り替える
標準で以下3種類のadapterが提供されていて、それらに切り替えることができる。
:attribuets (default)
:json
:json_api
rendering時のadapterパラメータに指定することで切り替え可能(※ グローバルな設定として持たせることも可能)。
# app/controllers/users_controller.rb
class UsersController < ApplicationController
def index
render json: User.all
end
def show
render json: User.find_by(id: params[:user_id]), adapter: :json # ★ ここ
end
end
アダプタごとに出力形式に特徴がある。
例えばJSONアダプタ(:json
)の場合はレスポンスのroot要素が作られるといった違いがある。
>> curl -s localhost:3000/users/1 | jq .
{
"user": {
"id": 1,
"name": "user-0",
"age": 1,
"decks": [
{
"id": 1,
"name": "deck-0"
},
{
"id": 11,
"name": "deck-10"
}
]
}
}
JSON APIアダプタ(:json_api
)は、jsonapiというJSON形式でデータを返すAPIの仕様を決めている団体?のようなものがあり、そこで作られたversion1.0の仕様に従っているようだ。
各アダプタの詳細は Adapters に書かれている。
※ 余談:JSONアダプタという名称にはちょっと違和感を感じる。デフォルトのattributesアダプタもJSON返してるし...。
独自の属性を追加する
メソッドを定義するなどして、Modelの属性としては存在しない独自の属性を追加できる。 以下は、fooというキー名でbarという値を返すよう、メソッドの定義とattributesメソッドの引数変更をしたときの例。
# app/models/user_serializer.rb
class UserSerializer < ActiveModel::Serializer
attributes :id, :name, :age, :foo # ★ ココと..
has_many :decks
# ★ ココ
def foo
"bar"
end
end
レスポンスは次のように変わる。
>> curl -s localhost:3000/users/1 | jq .
{
"id": 1,
"name": "user-0",
"age": 1,
"foo": "bar", # ★ 増えた
"decks": [
{
"id": 1,
"name": "deck-0"
},
{
"id": 11,
"name": "deck-10"
}
]
}
その他気になった機能
- 関連先のモデルは
include
オプションでも指定できるrender json: User.all, include: ['decks']
- includeの値として
*
を渡すと、関連先のモデルを1階層分だけ出力するrender json: User.all, include: "*"
- 同様に、
**
を渡すと、関連先のモデルを全て辿って出力するrender json: User.all, include: "**"
- ApplicationControllerのように、各シリアライザ共通の処理をApplicationSerializerなどとして括り出せる(クラスの名称は何でも良さそう)
- cache機能がある
- key_transform optionsオプションで、出力するキー名のフォーマットを変更できる
- camel, camel_lower, dash, unaltered, underscore, nil などが指定できる
- 例:
render json: User.all, key_transform: :camel
使ってみての雑感
直感的な設計になっていて、大体こんなかんじで動いてくれるだろうな〜っていうのがその通りに動いてくれる感じが良かった。
v1.0に向けて開発中のようだが、この辺の使い勝手の良さはそのまま維持されると嬉しい。