pockestrap

Programmer's memo

Rails.cache.fetch_multi するGraphQL BatchのLoader

Rails.cache.fetch_multiするLoaderを書いてみた。 コードのライセンスはCC-0とするのでご自由にお使いください。

class RailsCacheLoader < GraphQL::Batch::Loader
  def initialize
  end

  # @param args [Array<[untyped, Proc]>] The first item is cache key, and the second item is fallback.
  def perform(args)
    fallback_map = args.to_h
    result = Rails.cache.fetch_multi(*fallback_map.keys) do |key|
      fallback_map[key].call
    end

    result.each do |cache_key, value|
      fulfill([cache_key, value], value)
    end
  end

  def cache_key(arg)
    arg.first
  end
end

こう使う。

field :heavy_field, String, null: false, resolve: -> (obj, _args, context) {
  RailsCacheLoader.load([
    ["heavy_field", obj, context.fetch(:current_user)],
    -> () { obj.fetch_heavy_field(by: context.fetch(:current_user)) }
  ])
}

これは次のコードと同様に動作しつつ、Rails.cacheのバックエンド(memcachedなど)へのアクセスが1回で済む。

field :heavy_field, String, null: false, resolve: -> (obj, _args, context) {
  Rails.cache.fetch(["heavy_field", obj, context.fetch(:current_user)]) do
    obj.fetch_heavy_field(by: context.fetch(:current_user))
  end
}

手元のRails appで対象のfieldを100件fetchするクエリを試したところ、RailsCacheLoaderを使ってもRails.cache.fetchを書いてもほとんど速度差は出なかった。localhostに建っているmemcachedへのアクセスなので、差が出づらいのは予想できる。 本番環境には突っ込んでいないため、実際どの程度速くなるかは未確認。

本番環境に突っ込んでいないのはこのLoaderの問題ではなく、そもそも対象のfieldをキャッシュするべきか悩んでいるため。 微妙に遅いfieldの取得を速くするためだけにキャッシュに手を染めるべきか、微妙なところ。