概要
原著者の許諾を得て翻訳・公開いたします。
- 英語記事: Disassembling Rails — Fragment Caching – Stan Lo – Medium
- 原文公開日: 2018/06/23
- 著者: Stan Lo -- Goby言語の作者でありRails開発者です。
なお、本記事はRubyWeekly #405でも紹介されています。
Railsのフラグメントキャッシュを分解調査する(翻訳)
本記事は「Rails分解調査」シリーズの第一弾です。このシリーズのねらいは、Railsのコンポーネントを使うときにそれぞれの機能(今回のフラグメントキャッシュなど)が互いにどう絡み合うかという点について一般的な概念を示すことです。何ぶんこの種の記事を書くのは初めてですので、何かお気づきの点がありましたらぜひ元記事までコメントをどうぞ。
Railsのフラグメントキャッシュはほとんどの方がご存知か使ったことがあると思いますが、そのしくみについてはあまり知られていないのではないでしょうか。そこで本記事では、Railsの背後のフラグメントキャッシュのしくみについて簡単にご紹介したいと思います。
まず、フラグメントキャッシュの実行に関連する以下のコンポーネント(クラスやモジュール)を見ていきましょう。
もちろんここでは一般的な概念を示すにとどめますので、すべてのクラスやモジュールまではリストアップしていません。
ユーザーインターフェイス
フラグメントキャッシュの使い方はいたってシンプルです。次のようにcache
を呼ぶだけで完了します。
<% cache project do %>
<b>All the topics on this project</b>
<%= render project.topics %>
<% end %>
このcache
メソッドがあるのはActionView::Helpers::CacheHelper
というビューヘルパーです。
# File actionview/lib/action_view/helpers/cache_helper.rb, line 165
module ActionView
module Helpers
module CacheHelper
def cache(name = {}, options = {}, &block)
if controller.respond_to?(:perform_caching) && controller.perform_caching
name_options = options.slice(:skip_digest, :virtual_path)
safe_concat(fragment_for(cache_fragment_name(name, name_options), options, &block))
else
yield
end
nil
end
end
end
end
ここでは主に次の2つを行っています。
cache_fragment_name
でキャッシュのキーを生成するfragment_for
でこのブロックをキャッシュする
fragment_for
の動作を見てみましょう(訳注: これはprivateメソッドです)。
# actionview/lib/action_view/helpers/cache_helper.rb
def fragment_for(name = {}, options = nil, &block)
if content = read_fragment_for(name, options)
......
content
else
......
write_fragment_for(name, options, &block)
end
end
この動作も非常にシンプルで、キャッシュが存在する場合は内容を読み出し、なければコンテンツを書き込んでいるだけです。キャッシュの内容の読み書き以外に、以下のようにテンプレートのレンダラーにキャッシュのヒットの有無も通知します。
# actionview/lib/action_view/helpers/cache_helper.rb
def fragment_for(name = {}, options = nil, &block)
if content = read_fragment_for(name, options)
@view_renderer.cache_hits[@virtual_path] = :hit if defined?(@view_renderer)
......
else
@view_renderer.cache_hits[@virtual_path] = :miss if defined?(@view_renderer)
......
end
end
Railsはキャッシュヒットの有無をこのようにして認識します。そしてfragment_for
メソッドはキャッシュの内容の読み書きのためにread_fragment_for
とwrite_fragment_for
を使っています(訳注: これらもprivateメソッドです)。
# actionview/lib/action_view/helpers/cache_helper.rb
def read_fragment_for(name, options)
controller.read_fragment(name, options)
end
def write_fragment_for(name, options)
pos = output_buffer.length
yield
......
fragment = output_buffer.slice!(pos..-1)
......
controller.write_fragment(name, fragment, options)
end
ご覧のように、最終的にcontroller
という名前の変数に対してread_fragment
とwrite_fragment
が呼び出されています。ご推察のとおり、このcontroller
とは現在のリクエストを処理しているRailsのコントローラなのです。つまりここから主体がビューからコントローラに移ることになります。controller.write_fragment
のソースを追ってみると、AbstractController::Caching::Fragments
にあるのがわかります。
メインのロジック: AbstractController::Caching::Fragments
このモジュールが提供する本質的なキャッシュ操作はread_fragment
、write_fragment
、expire_fragment
の3つです。
# actionpack/lib/abstract_controller/caching/fragments.rb
# (instrumentation関連のコードは省略)
def write_fragment(key, content, options = nil)
......
content = content.to_str
cache_store.write(key, content, options) # <-ここでキャッシュの内容をキャッシュストアに書き込む
content
end
def read_fragment(key, options = nil)
......
result = cache_store.read(key, options) # <- ここでキャッシュの内容をキャッシュストアから読み出す
result.respond_to?(:html_safe) ? result.html_safe : result
end
def expire_fragment(key, options = nil)
......
if key.is_a?(Regexp)
cache_store.delete_matched(key, options) # <- ここでキャッシュストアから複数のキャッシュコンテンツを削除する
else
cache_store.delete(key, options) # <- ここでキャッシュストアからキャッシュコンテンツを削除
end
end
このモジュールは、ユーザーインターフェイス(cache
メソッド)とキャッシュストレージの中間に位置する抽象化レイヤと見なすこともできます。このモジュールがActionController::Base
によってinclude
されることで、コントローラ内でこれらのメソッドをすべて呼び出せるようになります。
根幹の部分: ActiveSupport::Cache::Store
次はActiveSupport::Cache::Store
というコンポーネントです。ここでは、redis
やmemcache
やfile
などさまざまなキャッシュストレージの汎用的なインターフェイスを定義しています。以下を実行してみるとわかります。
# Rails 5.2.0
ActiveSupport::Cache::Store.descendants
#=> [ActiveSupport::Cache::Strategy::LocalCache::LocalStore,
# ActiveSupport::Cache::FileStore,
# ActiveSupport::Cache::MemoryStore,
# ActiveSupport::Cache::RedisStore]
なお一部のクラスについては、対応するgemがインストールされていないとRailsで読み込めないため表示されません(例: ActiveSupport::Cache::MemCacheStore
の読み込みにはdalli
gemのインストールが必要)。
read
、write
、fetch
、delete
といったインターフェイスがよく使われますが、すべてを解説すると煩雑なので、ここではread
メソッドのみを例示するにとどめます。
# activesupport/lib/active_support/cache.rb
# (instrumentation関連のコードは省略)
def read(name, options = nil)
options = merged_options(options)
key = normalize_key(name, options)
version = normalize_version(name, options)
entry = read_entry(key, options)
if entry
if entry.expired?
delete_entry(key, options)
nil
elsif entry.mismatched?(version)
nil
else
entry.value
end
else
nil
end
end
end
上のread_entry
とdelete_entry
(他にwrite_entry
)メソッドは、各キャッシュストレージクラスでの実装に必要なインターフェイスです。
# activesupport/lib/active_support/cache.rb
def read_entry(key, options)
raise NotImplementedError.new
end
def write_entry(key, entry, options)
raise NotImplementedError.new
end
まとめ
本記事でコードを追ったことで、Railsを支えるフラグメントキャッシュの基本的な理解にお役に立てばと思います。お気づきの点やご意見がありましたらぜひ原文にコメントをお寄せください。このような記事をもっとお読みになりたい方もぜひお知らせください。がんばって書きますので。