結論
Rails.cache.fetch("page_#{page}", expires_in: 24.hours) do
items = Item.all.page(page).per(50)
Kaminari::PaginatableArray.new(items.to_a, limit: items.limit_value, offset: items.offset_value, total_count: items.total_count)
end
概要
Kaminariを使ってページネーションをしていて、データが膨大なのでキャッシュをすることになりました。
しかし、 Item.all
をキャッシュするのもまたデータが大きすぎるので、ページごとに絞り込んだ結果でキャッシュしたいということに。
いつも通りの方法で以下の様にやってみたところエラーが…
Rails.cache.fetch("page_#{page}", expires_in: 24.hours) do
items = Item.all.page(page).per(50)
end
=> can't dump anonymous class #<Module:0x00000001197139b8>
ということで調べました。
なぜエラーになるのか?
ChatGPTによると、エラーの原因は以下になります。
”TypeError: can't dump anonymous class
というエラーは、Railsのキャッシュ機構で、キャッシュに保存できない(シリアライズできない)オブジェクトを扱っている際に発生します。この場合、キャッシュ対象のオブジェクト(Item.al
l.page(page).per(50)
)の中に、キャッシュできない匿名クラスやモジュールが含まれている可能性があります。”
Kaminariのメソッドを使ったことで余計なデータが入ってしまった様です。
解決策
Kaminari::PaginatableArray
を使ってKaminariに互換性のある配列に変換します。
単純な配列であればキャッシュができるので、色々調べると絞り込んだデータを .to_a
で配列にする様言われました。
ですが、配列にすると今度は paginate
で使われる total_pages
などが使えなくなってしまうのが問題でした。
それをKaminariに互換性のある配列にすることでどちらの問題も解決することができます。
items = Item.all.page(page).per(50)
Kaminari::PaginatableArray.new(items.to_a, limit: items.limit_value, offset: items.offset_value, total_count: items.total_count)
各パラメータの解説は以下です。
- 第1引数:こちらは配列型にしたオブジェクトを渡します。
- limit:Kaminariが設定している1ページあたりの件数(
per(50)
の50
)を取得します。これにより、Kaminari::PaginatableArray
に「1ページあたりの件数」が渡されます。 - offset:現在のページの先頭から何件目までスキップするかを示します。例えば、2ページ目の場合、
per(50)
だとoffset_value
は50になります。これにより、正確にページのオフセット位置が適用されます。 - total_count:全体のレコード数を示します。これにより、ページネーションが全体で何ページ必要なのかを計算することができます。
最終的なコード
# controller
def index
@items = Rails.cache.fetch("page_#{page}", expires_in: 24.hours) do
items = Item.all.page(page).per(50)
Kaminari::PaginatableArray.new(items.to_a, limit: items.limit_value, offset: items.offset_value, total_count: items.total_count)
end
end
# view
<%= paginate @items %>
コメント
コメント失礼します
その方法でもいいと思い、一時期使ってたのですが
これだと二つ問題点があると思ってて
* キャッシュのサイズがobjectのサイズになるので、objectが大きいとキャッシュのサイズが大きくなる
* 例えばitemのidが100のタイトルのデータが変わった場合、そのキャッシュを削除したいと思った時、中身をわざわざ見る必要があり、削除しづらい
なのでこのようなキャッシュを作ることを考えました
page = 1
per = 50
cache_key = “pager_info_#{page}”
pager_info = Rails.cache.fetch(cache_key, expires_in: 24.hours) do
items = Item.all.page(page).per(per)
{
rows_ids: items.pluck(:id),
limit: items.limit_value,
offset: items.offset_value,
total_count: items.total_count
}
end
これだと使う時は
このようになります
ager_info = Rails.cache.read(cache_key)
ids = pager_info[:rows_ids]
rows = Item.where(id: ids)
# idsの並び順に意味があるため、fieldで並び替える
rows = rows.reorder(Arel.sql(“field(id, #{ids.join(‘,’)})”)) if ids.present?
limit = pager_info[:limit]
offset = pager_info[:offset]
total_count = pager_info[:total_count]
items = Kaminari::PaginatableArray.new(
rows.to_a,
limit:,
offset:,
total_count:
)
さきほどの問題点が解消し
キャッシュのサイズは小さくなるし、
itemのidが100のタイトルのデータが変わった場合も勝手に変わるので
多少DBのアクセスが発生しますが、使い勝手がよいかと思います