FastJSON API+OjでパフォーマンスのいいRailsAPIサーバーを作る

RailsをAPIサーバーとして動かすにあたり，どのようなライブラリを用いるのが良いのか調べた．

実際にパフォーマンスを簡単に測定したのでお伝えします．
参考リンクを末尾に記載するので結論の裏どりが欲しい方はそちらも参照ください．

# TL;DR

FastJSON API + Oj でJSONを返すのが最もパフォーマンスが良い


# 前提

JSONとはJavaScript Object Notationの略で，JSのObject型をStringで表現したものである．
String型なのでブラウザに受け渡しができる一方，
サーバー側ではObjectデータとして取り扱いやすいことから，広くAPIで一般的に用いられるデータの形となっている．

リクエストを受け取ったサーバーが，必要なデータをObjectからStringに変換する作業のことをJSON Serializationと呼ぶ．

このSerializationには大きく2つのプロセスに分解できる．
1. Data Preparation: サーバーサイドでJSONへ変換できる型にデータを整える(RubyではHash型になる)
2. Serialization: そのデータをJSONに変換する

それぞれどのような外部ライブラリがあり，パフォーマンスや注意点は何かを簡単に調べた．

# 検証した

パフォーマンスは `Benchmark`モジュールを利用した．
また，データはAuthorというモデルに適当に100個程度のレコードを作成してタネとした．もちろんお手持ちのデータなんでも良い．
なお実行はCPUを4GB割り当てたDockerコンテナで行っているため，実行時間に関する数字は相対的な比較に用いるものと思って欲しい．

## Data Preparation

検討したのはこの6つ

* JBuilder
* Active Model Serializer
* FastJSON API
* JSON API Serializer
* JSON API Resources
* RABL



### JBuilder

* 無知だった僕はてっきりこれで実装しようかと考えていた
* Documentは比較的例が多くて見やすい
* 記述方法にクセがあり，partialを用いてJSON書こうと思うとかなり面倒臭い
* パフォーマンスは悪い

```ruby
# app/views/author.json.jbuilder
json.id author.id
json.name author.name

# rails console
renderer = ApplicationController.new
Benchmark.measure do
  100.times do
    Author.all.each do |author|
      renderer.render_to_string('/author', locals: {author: author})
    end
  end
end
```
@real=1999.7994155000197
さすがにこれは悪すぎる．render部分にも時間がかかっているため，比較できないと言われればそれまでだが，明らかに遅かった．

### ActiveModelSerializer
* Qiitaの記事はこれについてが最も多いように思える
* パフォーマンスは悪くはない

```ruby
# Gemfile
gem 'active_model_serializers'

# app/serializers/author_serializer.rb
class AuthorSerializer < ActiveModel::Serializer
  has_many :posts

  attributes :id, :name
end
# `rails g serializer author` で生成される

# rails console
Benchmark.measure do
  100.times do
    Author.all.each do |author|
      puts AuthorSerializer.new(author).as_json
    end
  end
end
```
@real=2.2909035999909975

### Fast JSON

* Netflixが開発したデータオブジェクト生成gem.
* JSON:APIのフォームで記述される（[RESTとGraphQLとの中間的な構造だという考えもある](https://qiita.com/yoshixj/items/6499490f6fbefea05cae)）
* POSTでもGETでも，データ記述に制約があるため，クライアント側との相談が必要
* とはいうものの，性能は良い（[[Netflix Tec Blog](https://netflixtechblog.com/fast-json-api-serialization-with-ruby-on-rails-7c06578ad17f) | [和訳](https://qiita.com/rana_kualu/items/700284059cb0d893dc0e)]）

```ruby
# Gemfile
gem 'fast_jsonapi'

# app/serializers/author_serializer.rb
class AuthorSerializer
  include FastJsonapi::ObjectSerializer
  attributes :name
  has_many :posts
end

# rails console
Benchmark.measure do
  100.times do
    Author.all.each do |author|
      puts AuthorSerializer.new(author).serializable_hash
    end
  end
end
```

@real=1.669708500005072
たしかに早い

### JSON API Resource
* まず公式ドキュメントが最新になっていない（きれいなDocがあるが）
* なのでhelloworldがそもそも動かない
* イシューも上がっているのに一向に返事がない
* というわけで諦める

```ruby
# Gemfile
gem 'jsonapi-resources'

# app/serializers/author_resource.rb
class AuthorResource < JSONAPI::Resource
 has_many :posts
 attributes :name
end

# rails console
Benchmark.measure do
  100.times do
    Author.all.each do |author|
     JSONAPI::ResourceSerializer.new(AuthorResource).serialize_to_hash(AuthorResource.new(author, nil))
    end
  end
end
# doesn't work
```

### JSON API Rails

`gem 'jsonapi-rails`が環境変数に対してエラーを起こしてbundle installできなかったのでスキップする

### RABL

```ruby
# Gemfile
gem 'rabl'

# app/views/author.rabl
object @job

attributes :id, :name


# rails console
Benchmark.measure do
  100.times do
    Author.all.each do |author|
      Rabl.render(author, 'author', :view_path => 'app/views', :format => :hash)
    end
  end
end
```
@real=59.919160100020235
使う理由が思いつかない

#### 結果
FastJSON > AMS >> RABL >> JBuilder | JSON API Rails | JSON API Resources

## JSON Serialization

検討したのはこの3つ

* Oj
* JSON
* Yajl

#### パフォーマンスの計測
これらはHashをJSONに変換するだけなのでパフォーマンスを比較する方法が比較的簡易

```ruby
# Gemfile
gem 'oj'
gem 'yajl-rails'
gem 'json' #
```

これらを`bundle install`してRailsコンソールを開く．
最初に `yajl`をrequireしないとエラーになるので

```ruby
# rails console

> require 'yajl'

> hash = {
  "id": 0,
  "post": {
    "id": 0,
    "is_closed": true
  },
  "author": {
    "id": 0,
    "name": "string",
    "company": {
      "id": 0,
      "name": "string"
    },
    "books": [
      {
        "id": 0,
        "name": "string"
      }
    ]
  },
  "book": {
    "id": 0,
    "name": "string",
    "author": {
      "id": 0,
      "name": "string"
    }
  },
  "phases": [
    {
      "id": 0,
      "name": "string",
      "date": "2020-01-21T11:35:13.699Z",
      "order": 0,
      "is_completed": true
    }
  ],
  "fee": 0,
  "publish_date": "2020-01-21",
  "payment_date": "2020-01-21"
}
# 実際にはあと10行分のkey:valueを持っていたがここでは省略する

> Benchmark.bm do |x|
    x.report { 100.times { Oj.dump(hash) } }  
    x.report { 100.times { JSON.generate(hash) } }
    x.report { 100.times { Yajl::Encoder.encode(hash) } }
  end
```

####結果

```
@real=0.002964600018458441　# Oj
@real=0.005538500001421198  # JSON
@real=0.0038283999892883003 # yajl
```
ということでOjが一番早かった

＊一応他の文献も調べると，他でもOjが最も早いと記述

なおFastJson APIはOjをデフォルトで利用している

# 実装の留意点

上記の通り，　FastJSON + Ojという組み合わせが最もパフォーマンスに優れている．

パフォーマンスに秀でているFastJSON APIだが，留意点としてはJSON:APIという記述の方法でJSONga吐き出されるため，フロントエンドでの受け取ったデータの扱い方が多少煩雑になるということである．フロントエンドエンジニアと分けて開発している場合はしっかり事前に相談しよう．

Ojを用いる時には，少しだけ特殊な挙動があるので注意が必要．このブログに詳細が書かれているが，要は`":hoge"`のようにストリングにコロンが入っているとシンボルに変換されてしまうというもの
https://qiita.com/ts-3156/items/b02d32dd625273d2e342

と書かせてもらいましたが，結局のところは
そもそもNがいくつくらいのデータ量をJSONでやりとりしようとしているのか考えて，
パフォーマンスを優先するのが望まれるのか，フロントエンドの柔軟性を重視するのかエンジニアチームで相談するのがいいのだと思います．

# 参考リンク

[ 1 ] *JSON Serialization in Rails: A Complete Guide (Jun 15, 2018)*
何はともあれこれを見て欲しい．
https://buttercms.com/blog/json-serialization-in-rails-a-complete-guide

[ 2 ] *Fast JSON API serialization with Ruby on Rails (Feb 1, 2018) | Netflix Technology Blog*
NetflixがFastJSONの優位性を主張するブログ．再現用のソースコードもある
https://netflixtechblog.com/fast-json-api-serialization-with-ruby-on-rails-7c06578ad17f
（なんか和訳した人がいる：https://qiita.com/rana_kualu/items/700284059cb0d893dc0e）

[ 3 ] *Fast JSONAPI vs AMS with OJ (Feb 2, 2018)*
FastJSONのJSON:APIフォーマットが嫌ならAMS＋Ojがいいと思ったが当然すでに調べている人がいる
https://medium.com/@yukikawamoto/fast-jsonapi-vs-ams-with-oj-ef9e10997ebd

[ 4 ] *Fast JSON API をつかって得た、JSON:APIの知見 (Dec 21, 2019)*
じゃあFastJSONを現実に使う時にフォーマットが面倒だなということに触れている記事
https://qiita.com/yoshixj/items/6499490f6fbefea05cae

[ 5 ] *RailsデフォルトのJSONレンダリングは遅いので注意。Ruby/RailsでJSON生成時に最もパフォーマンスが良い方法を調べてみた。(Jan 30, 2019)*
やっぱりOjは性能よし
https://qiita.com/ttiger55/items/d144b8094d61b70955bf