MNTSQ Techブログ

リーガルテック・カンパニー「MNTSQ(モンテスキュー)」のTechブログです。

RailsでlazyにN+1回避したい

MNTSQ Tech Blog TOP > 記事一覧 > RailsでlazyにN+1回避したい

f:id:hagi917:20210329143047j:plain

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

実家の猫をリモートで愛でる毎日です