【Rails】Kaminariでページネーションしたデータをページごとキャッシュする方法

RubyOnRails

結論

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.all.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 %>

コメント

  1. たっくん より:

    コメント失礼します

    その方法でもいいと思い、一時期使ってたのですが
    これだと二つ問題点があると思ってて
    * キャッシュのサイズが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のアクセスが発生しますが、使い勝手がよいかと思います

タイトルとURLをコピーしました