猫Rails

ねこー🐈

RequestStoreの使い方 まとめ

  • 自分用のメモを公開したものです。ドキュメント/ソースコード等をまとめただけで試していないコードも多いので、信頼性は低いです。

request_storeとは?

  • リクエスト毎にグローバルな変数を使える
  • ☆975

導入

1. インストール

gem 'request_store'
$ bundle

2. 使ってみる

# ApplicationController
before_action :set_current_user
def set_current_user
  RequestStore.store[:current_user] = current_user
end

# モデル
RequestStore.store[:current_user] #=> current_user

類似機能との比較

Thread.current

  • スレッドローカル変数。つまりスレッド単位でグローバルな変数。(ドキュメントにはnot thread-local but fiber-localと書いてあったが違いがわからんかった。fiber使わんし難しい...)
  • request_storeは内部でコレ使ってる
# Thread.current: 現在のスレッド
# Thread.current[:foo] 現在のスレッドにおいて、グローバルな変数
Thread.current[:foo] = 0
Thread.current[:foo] #=> 0

問題点

  • アプリサーバはリクエスト1回分でスレッドが終了せず、次のリクエストも同じスレッドで処理する。その際に値がリセットされていないので、前回の値を持ち越してしまう
# 毎回0にリセットされず、1,2,3,...と増えていく
def index
  Thread.current[:counter] ||= 0
  Thread.current[:counter] += 1

  render :text => Thread.current[:counter]
end

Webrickでは問題ないらしい

ActiveSupport::CurrentAttributes

  • リクエスト毎にリセットされるスレッドローカルな属性を定義できる
  • こちらも内部でThread.currentを利用してる
  • ユースケースとしてはrequest_id等を想定してるっぽい
  • Rails5.2で追加
  • 参考: https://github.com/rails/rails/pull/29180

使い方

  • たぶんこんな感じ?自信なし
# app/models/current.rb
class Current < ActiveSupport::CurrentAttributes
  # 属性定義。この属性はリクエスト事にリセットされる
  attribute :user
end


# before_actionで`Current.user`をセット。これでどこからでも`Current.user`でcurrent_userにアクセスできるようになる
class ApplicationController < ActionController::Base
  before_action :set_current_user

  def set_current_user
    Current.user = currrent_user
  end
end

# モデル
Current.user #=> currrent_user

使う際の注意点

グルーバルにアクセスできちゃう

  • グルーバルにアクセスできちゃうので、
    • MVCが壊れてカオスになる
    • テストが難しくなる
  • コレが必要になる場合は設計が間違っている可能性があるので、まずは設計を見直す
  • CurrentAttributesの記事だけど、同じことが当てはまりそう: https://techracho.bpsinc.jp/hachi8833/2017_08_01/43810

リクエスト毎にマルチスレッド使うのはNG

テスト

  • ミドルウェアを追加する必要あり
# spec_helper.rb
def app
  Rack::Builder.new do
    use RequestStore::Middleware
    run MyApp
  end
end

Rails以外で使う

  • ミドルウェアを追加すればOK
use RequestStore::Middleware

ざっくりコードリーディング

  • コードベースはかなり小さい

request_store.gemspec

  • 依存gem
    • rack: Rackミドルウェアを使うため

lib/request_store.rb

  • RequestStore
  • Thread.current[:request_store]を便利に扱えるようにしたラッパーのようなクラス
module RequestStore
  # `RequestStore.store[:foo]`は`Thread.current[:request_store][:foo]`に相当
  def self.store
    Thread.current[:request_store] ||= {}
  end

  ...
end

lib/request_store/middleware.rb

  • RequestStore::Middleware
  • レスポンス時にThread.current[:request_store]をクリアするRackミドルウェア
    # callはRackミドルウェアの規約
    def call(env)
      # リクエストの処理
      # フラグ立てる
      RequestStore.begin!

      # Rackアプリ本体の処理
      response = @app.call(env)

      # リクエスト時の処理
      # フラグ折る + データをクリア
      returned = response << Rack::BodyProxy.new(response.pop) do
        RequestStore.end!
        RequestStore.clear!
      end
    ensure
      unless returned
        RequestStore.end!
        RequestStore.clear!
      end
    end

lib/request_store/railtie.rb

  • ミドルウェアを追加したり

参考URL