preloadはけっこう難しい
mntsqのソフトウェアエンジニアチーム所属のhagiwaraです。
RailsアプリケーションのパフォーマンスチューニングとしてN+1問題を潰すというのはよく行われます。
教科書的には簡単に書けるのですが、現実のアプリケーション開発ではpreloadで頭を悩ませることがあります。
長く開発されてきたRailsアプリケーションは、さまざまな歴史的経緯があり、モデルのリレーションツリー構造が深く、特定の条件下でのみ必要になるテーブルがあちこちにぶら下がっているということが珍しくありません。
preloadは一般的にcontrollerの最終段階で行うことになると思いますが、コードベースが巨大だとリレーションのpreloadの要不要の判定が難しいことがあります。 そんな中で、可能性のあるリレーションを全て保守的にeagar_load/preloadした結果、パフォーマンスに問題を抱えているアプリケーションは決して少なくないという現実があります。
ar_lazy_preload gemを試してみる
preloadをlazyに行うことで、上のお悩みを軽減できる可能性のあるgemです。
以下のようなmodelを用意して実験しました
# Rails 6.1.3 / ruby 3.0.0 class User < ApplicationRecord has_many :boards end class Board < ApplicationRecord belongs_to :user end User.count #=> 10 Board.count #=> 100 (各userに10)
まずは素のRailsの動作から
users = User.all # どのuserのboardsもpreloadされていない users.any? { |u| u.boards.loaded? } # => false # 一つのユーザーのboardを一つ参照してみる users[0].boards[0] # user[0]以外のboardsはロードされていない users.all? { |u| u.boards.loaded? } # => false
lazy_preloadを使用して動作を見ます
users = User.lazy_preload(:boards).all # どのuserのboardsもpreloadされていない users.any? { |u| u.boards.loaded? } # => false # 一つのユーザーのboardを一つ参照してみる users[0].boards[0] # 全てのuserのboardsがpreloadされている users.all? { |u| u.boards.loaded? } # => true
4層のような深い子孫関係にある場合でも期待した動作をしています。
- クエリの自由度が高く、クエリパラメータによってpreloadしなきゃいけないものが変化する
- テンプレートや埋め込む変数が動的に変動して予測が難しい
などのケースで保守的に全部preload刺していたようなところは少し楽ができそうです。
この記事を書いた人
hagiwara
実家の猫をリモートで愛でる毎日です