ActiveRecordのキャッシュ徹底解説:データベースへの不要なアクセスを避ける仕組み
キャッシュとは、一般的に「コードの実行結果を保存しておき、後で素早く取得できるようにする仕組み」を指します。計算済みの値を保存して再計算を避けるケースもあれば、ハードディスクへの読み込みやネットワークリクエストを避けるために、単にデータをメモリ上に保持しておくケースもあります。
後者の形は、ActiveRecordにとって特に重要です。多くの場合、データベースはアプリケーションサーバーとは別のサーバー上で稼働しているため、すべてのリクエストにはネットワーク通信のオーバーヘッドが発生しますし、同じクエリを再実行すればデータベースサーバーにも余計な負荷がかかります。
幸いなことに、Rails開発者にとってActiveRecordはこうした処理の多くを、私たちが意識しないうちに自動的に担ってくれます。これは生産性の面で非常にありがたいのですが、裏側で何がキャッシュされているのかを把握しておくことも重要です。たとえば、別のプロセス(バックグラウンドジョブなど)によって値が変更されることが分かっている場合や、どうしても最新の値が必要な場合などです。そうした場面のために、ActiveRecordにはキャッシュを無視して強制的にデータを読み込むための「脱出ハッチ(escape hatch)」がいくつか用意されています。
ActiveRecordの遅延評価(Lazy Evaluation)
ActiveRecordの遅延評価は厳密にはキャッシュではありませんが、後のコード例でも登場するため、簡単に概要を説明します。ActiveRecordのクエリを構築しても、多くの場合、その時点ではデータベースへの呼び出しは発生しません。これにより、複数の.where句をチェーンしても、その都度データベースにアクセスせずに済みます:
@posts = Post.where(published: true)
# まだDBアクセスなし
@posts = @posts.where(published_at: Date.today)
# それでもまだ何も起きない
@posts.count
# SELECT COUNT(*) FROM "posts" WHERE...
ただし例外もあります。.find、.find_by、.pluck、.to_a、.firstなどを使用した場合、追加の条件句をチェーンすることはできません。以降の例では、DBアクセスを強制するシンプルな方法として.to_aを使うことが多くなります。
なお、Railsコンソールでこの挙動を試す場合は、「echo」モードをオフにする必要があります。echoモードが有効だと、Enterキーを押した時点でコンソール(irbまたはpry)がオブジェクトに対して.inspectを呼び出し、DBクエリが強制的に実行されてしまうためです。echoモードを無効化するには、次のコードを使用します:
conf.echo = false # irbの場合
pry_instance.config.print = proc {} # pryの場合
ActiveRecordの関連付け(Relations)
ActiveRecordの組み込みキャッシュの最初のポイントは、関連付け(relations)です。典型的なUser-Postsの関係を見てみましょう:
# app/models/user.rb
class User < ApplicationRecord
has_many :posts
end
# app/models/post.rb
class Post < ApplicationRecord
belongs_to :user
end
これにより、user.postsやpost.userという便利なメソッドが使えるようになり、関連するレコードを検索するDBクエリを実行できます。コントローラとビューでこれらを使う例を見てみましょう:
# app/controllers/posts_controller.rb
class PostsController < ApplicationController
def index
@user = User.find(params[:user_id])
@posts = @user.posts
end
...
# app/views/posts/index.html.erb
...
<%= render 'shared/sidebar' %>
<% @posts.each do |post| %>
<%= render post %>
<% end %>
# app/views/shared/_sidebar.html.erb
...
<% @posts.each do |post| %>
<li><%= post.title %></li>
<% end %>
基本的なindexアクションで@user.postsを取得しています。前のセクションと同様に、この時点ではDBクエリは実行されていません。その後Railsがindexビューをレンダリングし、ビュー内でサイドバーがレンダリングされます。サイドバーで@posts.each ...が呼び出された時点で、初めてActiveRecordがDBクエリを発行してデータを取得します。
そしてindexテンプレートの残りの部分に戻ると、そこにはもう一つの@posts.eachがあります。しかし今回はDB呼び出しは発生しません。ActiveRecordがこれらの投稿をすべてキャッシュしており、再度データベースから読み込もうとしないのです。
脱出ハッチ(Escape Hatch)
関連レコードを強制的に再取得したい場合もあります。別のプロセス(バックグラウンドジョブなど)によってレコードが変更されることが分かっている場合や、自動テストで最新のDBの値を取得して、コードが正しく更新したことを検証したい場合などがよくある例です。
状況に応じて2つの方法があります。最も一般的なのは、関連付けに対して.reloadを呼び出す方法です。これはActiveRecordに対して「キャッシュを無視してDBから最新版を取得してほしい」と伝えます:
@user = User.find(1)
@user.posts # DB呼び出し
@user.posts # キャッシュ済み、DB呼び出しなし
@user.posts.reload # DB呼び出し
@user.posts # 新しいバージョンがキャッシュ済み、DB呼び出しなし
もう一つの選択肢は、ActiveRecordモデルの新しいインスタンスを取得することです(例:findを再度呼び出す):
@user = User.find(1)
@user.posts # DB呼び出し
@user.posts # キャッシュ済み、DB呼び出しなし
@user = User.find(1) # @userは新しいUserインスタンスになる
@user.posts # DB呼び出し、このインスタンスにはキャッシュがない
関連付けのキャッシュは便利ですが、単純な関連ルックアップ以上の複雑な.where(...)クエリを書くことも多いでしょう。ここで登場するのがActiveRecordのSQLキャッシュです。
ActiveRecordのSQLキャッシュ
ActiveRecordはパフォーマンス向上のため、実行したクエリの内部キャッシュを保持しています。ただし、このキャッシュは特定のアクションに紐づいている点に注意してください。アクションの開始時に作成され、終了時に破棄されます。つまり、同一のコントローラアクション内で同じクエリを2回実行した場合にのみ効果があり、Railsコンソールでは使用されません。キャッシュヒットはRailsログにCACHEとして表示されます。例えば、
class PostsController < ApplicationController
def index
...
Post.all.to_a # to_aでDBクエリを強制
...
Post.all.to_a # to_aでDBクエリを強制
end
end
このログ出力は以下のようになります:
Post Load (2.1ms) SELECT "posts".* FROM "posts"
↳ app/controllers/posts_controller.rb:11:in `index'
CACHE Post Load (0.0ms) SELECT "posts".* FROM "posts"
↳ app/controllers/posts_controller.rb:13:in `index'
ActiveRecord::Base.connection.query_cacheを出力すると、アクションのキャッシュの中身を覗くこともできます(SQLクエリだけが必要ならActiveRecord::Base.connection.query_cache.keysを使用します)。
脱出ハッチ(Escape Hatch)
SQLキャッシュをバイパスする必要があるケースはあまり多くないかもしれませんが、ActiveRecord::Baseのuncachedメソッドを使えば、ActiveRecordにSQLキャッシュを無視させることができます:
class PostsController < ApplicationController
def index
...
Post.all.to_a # to_aでDBクエリを強制
...
ActiveRecord::Base.uncached do
Post.all.to_a # to_aでDBクエリを強制
end
end
end
uncachedはActiveRecord::Baseのメソッドなので、可読性を高めたい場合はモデルクラス経由で呼び出すこともできます:
Post.uncached do
Post.all.to_a
end
カウンターキャッシュ(Counter Cache)
Webアプリケーションでは、関連レコードの数を数えたい場面が非常によくあります(ユーザーの投稿数、チームアカウントのメンバー数など)。あまりに一般的なため、ActiveRecordにはカウンターを自動的に最新状態に保つ仕組みが組み込まれており、大量の.count呼び出しがデータベースリソースを浪費することを防げます。有効化にはほんの数ステップしか必要ありません。まず、関連付けにcounter_cacheを追加して、ActiveRecordにカウントをキャッシュするよう指示します:
class Post < ApplicationRecord
belongs_to :user, counter_cache: true
end
次に、カウントを保存する新しいカラムをUserに追加します。この例ではUser.posts_countになります。必要であれば、counter_cacheにシンボルを渡してカラム名を指定することもできます。
rails generate migration AddPostsCountToUsers posts_count:integer
rails db:migrate
カウンターはデフォルト値の0に設定されます。既存の投稿があるアプリケーションでは、値を更新する必要があります。ActiveRecordにはreset_countersメソッドが用意されているので、IDと更新対象のカウンターを渡すだけで、細かい処理は任せられます:
User.all.each do |user|
User.reset_counters(user.id, :posts)
end
最後に、このカウントを使用している箇所を確認しましょう。.countを呼び出すとカウンターがバイパスされ、常にCOUNT() SQLクエリが実行されるためです。代わりに.sizeを使いましょう。.sizeはカウンターキャッシュが存在する場合、それを利用することを知っています。余談ですが、.sizeは関連付けが既に読み込まれている場合に再読み込みしないため、DBアクセスを節約できる可能性もあります。どこでも.sizeをデフォルトにする価値は十分にあるでしょう。
まとめ
ActiveRecordの内部キャッシュは、ほとんどの場合「そのまま動いてくれる」ものです。バイパスが必要なケースにはあまり遭遇していませんが、あらゆることと同様、「フードの下」で何が起きているかを知っておけば、特殊な対応が必要な状況に直面したときに、時間と苦労を大幅に節約できるでしょう。
もちろん、Railsが裏側でキャッシュを行っているのはデータベースだけではありません。HTTP仕様には、変更されていないデータの再送信を避けるためにクライアントとサーバー間で送受信できるヘッダーが含まれています。このキャッシュシリーズの次回記事では、304 (Not Modified) HTTPステータスコード、Railsがそれをどのように処理するか、そしてその処理をどのようにカスタマイズできるかを見ていきます。
-
Windows 10でF1ヘルプキーを無効化する方法|誤操作によるヘルプ画面の起動を防ぐ
Windowsキーボードのファンクションキー(F1〜F12)には、それぞれ特定の役割が割り当てられています。たとえばF1キーを押すと、現在開いているアプリケーションのヘルプページが表示されます。多くの場面で便利な機能ですが、状況によっては無効にしたいこともあるでしょう。残念ながら、Windows OSには標準でF1キーを無効化する機能は用意されていません。しかし、いくつかの方法でこれを実現できます。この記事では、キーボードのF1キーを一時的に無効化する方法と、後から再度有効化する方法を複数ご紹介します。キーボードのF1ヘルプキーを無効化するここで紹介する手順では、レジストリエディターなどWin
-
先延ばしグセを克服!スマホで使えるおすすめ集中アプリ5選
私たちは日常のさまざまな場面で、ついSNSアプリを開いてしまいがちです。スマートフォンは生活に欠かせない存在となりましたが、やるべき作業を後回しにする原因になっていることも少なくありません。 物事を先延ばしにする癖は大きな問題であり、この習慣があると目標達成は遠のいてしまいます。スマホの利用が増えた現代では、この傾向はさらに顕著になっています。しかし興味深いことに、解決策もまたスマホの中にあります。先延ばし癖を克服するためのアプリが数多く存在するのです。 スマホへの依存は、家族や友人と過ごす時間の大切さを見失わせるほど私たちを変えてしまいました。だからこそ、この習慣を断ち切り、時間をより賢く使