🚵‍♂️

Rails 遅延実行による間違ったキャッシュの使われ方

2024/09/23に公開

はじめに

初めまして!
webジニアです。今回の記事は、私が業務でコードリーディングをしているときに見かけた間違ったキャッシュの使われ方について解説します。
もし、タイトルにある「ActiveRecord の遅延実行」というフレーズがピンときていない場合はこの記事を読むことに意味があるかもしれません。
それではみていきましょう!

ActiveRecord の遅延実行

まずは、ActiveRecord の遅延実行について解説します。
ActiveRecord の遅延実行とは、データベースへのクエリ実行を実際にデータが必要になるときまで遅らせる仕組みのことです。これにより、不要なクエリの実行を避けてパフォーマンスの向上を図れます。
下記コードでは、実際にどのタイミングでクエリが実行されるでしょうか?

# Userモデルから全てのレコードを取得するクエリを作成
users = User.all # ①

# usersの各要素に対して繰り返し処理を行う
users.each do |user|
  puts user.name  # ②
end

先ほどの説明からも分かる通り、実際にクエリが実行されるのは②のタイミングになります。
このActiveRecord の遅延実行というのは、その名の通り、ActiveRecord自体に実装されています。
そのため、Railsを使うエンジニアならしっかり理解しておかなければなりません。
下記の記事に非常に詳しくかつわかりやすくまとめられていたので参考にしてみください!

https://zenn.dev/ebina_shohei/articles/1a4684ab47ea35

遅延実行が考慮されていない Rails.cache.fetch の使われ方

Rails.cache.fetch

まず、簡単にRails.cache.fetchについて簡単に説明します。
Rails.cache.fetchは、Railsアプリケーションでキャッシュを扱うための基本的なメソッドです。指定したキーに対応する値がキャッシュに存在する場合はそれを返し、存在しない場合はブロック内の処理を実行してその結果をキャッシュに保存し、その値を返すというものです。

Rails.cache.fetch(key, options = {}) do
  # キャッシュが存在しない場合に実行される処理
  # ここでキャッシュしたい値を返す
end
# optionには主にキャッシュの時間などを入れます

間違った Rails.cache.fetch の使われ方

キャッシュを使う一つの用途に、複数回使用する大きいデータをキャッシュに保存してデータベースへのアクセス負荷を減少させ、アプリの速度改善をするというものがよく見かけられます。
私がコードリーディングをしていて、見かけたものもおそらく、上記のようにアプリの速度改善やアクセス負荷の減少を狙ったものだったと思いますが、間違った使われ方がしていました。
それが下記のようなコードです。

def expensive_query
  User.where(active: true).order(:created_at).limit(10) 
end

def get_active_users
  Rails.cache.fetch('active_users', expires_in: 1.hour) do
    expensive_query 
  end
end

このコードの問題点は、expensive_query が返す ActiveRecord::Relation オブジェクトが即座にデータベースに問い合わせを行うのではなく、遅延実行されることです。 Rails.cache.fetch のブロック内で expensive_query を呼び出していますが、この時点ではまだクエリは実行されません。

get_active_users が初めて呼び出されたときに Rails.cache.fetch のブロックが実行され、expensive_query の結果がキャッシュされます。しかし、このとき初めてデータベースへのクエリが実行されるため、キャッシュの目的であるデータベースへの負荷軽減や速度改善の効果が得られません。

解決策

このような時の解決策はいくつかあります。
目的に応じて適切な手段を取りましょう。

①loadなどを用いてクエリをその場で実行させる
load などのメソッドを呼び出すことで、即座にクエリを実行し、結果を配列として取得することができます。この方法であれば、本来やりたかったキャッシュができるようになり、速度改善に役立ちます。

②実際にクエリが実行される箇所をキャッシュする
この方法は、ここでのキャッシュは諦めて、expensive_queryメソッドが呼ばれた後のデータをキャッシュするということです。使用範囲が限られていたりする場合は、この方法でもいいかもしれません。しかし、保守運用のことを考えると、変更があった際に実際に呼び出されている末端の箇所をいくつも修正しなければいけなくなるのでやや面倒かもしれません。

Discussion