redis-objectsでrails APIにキャッシュ機構を追加する

# これはなに
- rails APIにredisによるキャッシュ機構を導入する際、接続するredisの設定や各キャッシュの設定（キー・期限など）を隠蔽してくれる[redis-objects](https://github.com/nateware/redis-objects)が役に立ちます。今回、RailsAPIにRedisキャッシュ機構を実装したので、メモを残しておきます。
- 検証時の各種ライブラリのバージョンは以下です
    - redis-objects 1.4.3
    - rails 5.2.3
    - ruby 2.6.1p33


# 前提
## redisとは
インメモリKVS。データの格納・取得が鬼早い。詳しくはググってください。
なお、今回の記事では記載していませんが、redis-objectの導入前に、redis-cliを用いて色々コマンドを触ってみると、より理解が深まる気がします。

## キャッシュとは
APIに飛んできたリクエストに対して毎回DBにデータを取りに行くのは非効率なので、同じパラメータを持つリクエストへのレスポンスはAPIサーバー側で保持しておいて、2回目以降のリクエストに対してはそれを返しちゃえばいいじゃん、というやつ。DBへのアクセス数が減る分、DBサーバーの負荷軽減、レスポンス速度の向上、などの効果が見込まれる。
気をつけないといけない点としては、キャッシュに有効期限を設定しないと、コンテンツ側の変更が反映されなかったり、キャッシュサーバーの容量が圧迫されたりするので、いい感じ（今回は1時間）の有効期限を設定してあげるとよい。

## 今回導入するキャッシュ機構
*Cache-Aside Pattern* と呼ばれるキャッシュパターンを採用しました。

- APIに対してリクエストがきた際、特定のルールに基づいてパラメータからキー（文字列）を生成
- そのキーに対応するキャッシュがあるかredisサーバーに問い合わせる
    - あった場合
        - redisサーバーから受け取った値を（必要に応じて整形し）レスポンスとして返す
    - なかった場合
        - DBに値を問い合わせ、先ほど生成したキーとDBから受け取った値をredisサーバーに格納
        - DBから受け取った値をレスポンスとして返す

という感じです。

# 導入手順
## 要件
- `/items?item_category_id=${カテゴリID}`というエンドポイントへのリクエストに対するレスポンスを、カテゴリIDごとにJSON型でキャッシュする
- キャッシュの期限は1時間とする

## redis-objectsの導入
```ruby:gemfile
gem "redis-objects", "~> 1.4.3"
```
```plaintext:terminal
bundle install
```

## redis-objects用の設定追加
- 今回のプロジェクトには[global](https://github.com/railsware/global)が導入されていたので、`Global.redis.host` という感じに環境ごとのホストとポートの設定を読み込めるよう、設定ファイルを `config/globals/` に追加します
- 今回の記事では本番環境などの場合の環境変数は省略しますが、必要な場合は適当に追加してください。

```yml:config/globals/redis.yml
default: &default
  host: <%= ENV.fetch("REDIS_HOST", "redis") %>
  port: <%= ENV.fetch("REDIS_PORT", "6379") %> # 6379はredisのデフォルトポート番号

development:
  <<: *default

test:
  <<: *default
```

- Railsアプリ起動時にRedisObjectとRedisサーバーの紐付けを行うために、`config/initializers`にredisの接続設定を記載します

```ruby:config/initializers/redis.rb
host = Global.redis.host
port = Global.redis.port
Redis::Objects.redis = Redis.new(host: host, port: port)
```

## キャッシュ用のクラス作成
- 今回はレスポンス用のJSONをまるごとキャッシュするので、`model/`配下にredis-objectsをincludesした`*Response.rb`というクラスを作成します

```ruby:app/models/items_response.rb
class ItemsResponse
  include Redis::Objects

  value :resource, expireat: -> { Time.current + 1.hour } # 有効期限1時間の設定

  def initialize(item_category_id)
    @item_category_id = item_category_id
  end

  def id # redis-objectがキーを生成するのに使用する。設定するキャッシュ単位ごとに一意である必要がある
    @item_category_id
  end
end
```

## コントローラー側でキャッシュ機能作成
```ruby:app/controllers/items_controller.rb
class ItemsController < ApplicationController
  def show
    if cache_exists
      result.resource = JSON.parse(@cached_value)
    else
      ... # DBからitemsを取得する処理
      store_cache(result.resource)
    end
    render json: result.resource
  end

  private

  def cache_exists
    @cached_value = ItemsResponse.new(params[:item_id]).resource.value
  end

  def store_cache(resource)
    ItemsResponse.new(params[:item_id]).resource = resource.to_json
  end
end
```

# その他
## キーの変更
- redisに格納する際のキーはデフォルトでは`"#{モデル名}:#{self.id}:#{フィールド名}"`ですが、これを変えるときは、valueの宣言時にkeyオプションを渡すことでキーの変更が可能です。

```ruby:ラムダを渡してキー変更
class ItemsResponse
  include Redis::Objects

  value :resource, key: ->(instance) { "it:is:new:key:#{instance.id}:good" }
  ...
end
```

```ruby:文字列を渡してキー変更
class ItemsResponse
  include Redis::Objectsfff

  value :resource, key: 'it:is:new:key:#{instance.id}:good' # 文字列はシングルクオートで囲って渡す点に注意
  ...
end
```

## redis DBの変更
アプリケーション内で複数のredisキャッシュ用クラスを持つ場合、クラスごとにredis dbの切り替えを行うことが可能です。
 
```ruby:items_response.rb
class ItemsRespnose
  include Redis::Objects
  ...
end
```

```ruby:users_response.rb
class UsersRespnose
  include Redis::Objects
  ...
end
```

```ruby:redis.rb
host = Global.redis.host
port = Global.redis.port
ItemsResponse.redis = Redis.new(host: host, port: port, db: 0) # デフォルトは0
UsersResponse.redis = Redis.new(host: host, port: port, db: 1)
```