【Ruby on Rails】eager_loadって何だろう
はじめに
こんにちは!気づいたら今年ももう終わりますね!
社会人になってから時間が高速で進んでる気がしてるのは僕だけでしょうか?
時を高速で進めてる人
という事で最近バックエンドのコードを触る機会が増えてきたのですが、Railsのコードを読んでてた際に出てきたeager_loadというものが何か分からなかったので調べてみました。
N+1問題とは
eager_loadの説明の前に、まずN+1問題の話をします。
関連データを取得するたびにクエリが発行されることで、余分なクエリが発生し、パフォーマンスに影響が出ることです。
userモデルとpostモデルがあり以下のようなアソシエーションを組んでたとします。
class User < ApplicationRecord
has_many :posts
end
class Post < ApplicationRecord
belongs_to :user
end
そして以下のようなコードを書くと、usersの情報を取得するのに1クエリ発行し、
リレーションを組んでるpostsのレコードを取得する際に、10クエリ発行するので、合計11クエリ発行されてしまいます。(N+1というか1+Nっぽい)
# 1クエリ発行する
users = User.limit(10)
# 10クエリ発行される
users.each do |user|
puts user.posts.count
end
eager_loadとは
eager_loadを使うと、関連するテーブルをLEFT OUTER JOINで結合し、キャッシュします。
最初に関連するテーブルを結合しているので、クエリの発行量が1回で済むようになります。
# この1回で済む
users = User.eager_load(:posts)
users.each do |user|
puts user.posts.count
end
SELECT users.*, posts.*
FROM users
LEFT OUTER JOIN posts ON posts.user_id = users.id;
また、事前にテーブル同士をjoinしてるので、アソシエーションを使った絞り込みもできます。
この場合、postのidが1に該当するuserのレコードを取ってきます。
User.eager_load(:posts).where(posts: { id: 1 })
なのでeager_loadがしてることは、N+1問題を回避するためのものぐらいの認識で良さそうです。
他にもあったN+1問題を回避するためのメソッド
preload
preloadを使うと、関連データを別々のクエリで取得します。
テーブル結合は行われませんが、キャッシュはされます。
SELECT * FROM users;
SELECT * FROM posts WHERE posts.user_id IN (1, 2, 3, ...);
ただしテーブル同士をjoinしてないので、where句でアソシエーションの値で絞ることができません。
# UserからPostのidをwhereで引っ張って来れない
User.preload(:posts).where(posts: { id: 1 })
includes
eager_loadとpreloadの両方の機能を兼ね備えたメソッドです。
Railsが状況に応じてどちらを使用するかを自動で判断します。
なので、以下のようにwhereで絞り込みをするとeager_loadの挙動をします
users = User.includes(:posts).where(posts: { name: "hello" })
users.each do |user|
puts user.name
end
SELECT users.*, posts.*
FROM users
LEFT OUTER JOIN posts ON posts.user_id = users.id
WHERE posts.name = 'hello';
対して関連テーブルに対して、条件指定などがない場合はpreloadの挙動をします
users = User.includes(:posts).limit(10)
users.each do |user|
puts user.posts.count
end
SELECT * FROM users LIMIT 10;
SELECT * FROM posts WHERE posts.user_id IN (1, 2, 3, ..., 10);
joins / left_outer_joins
調べてたらjoins、left_outer_joinsというメソッドもありました。
これは名前の通り、SQLのINNER JOINとLEFT OUTER JOINをActiveRecordで使えるようにしたものです。
※ LEFT OUTER JOINの例は割愛
# 投稿が存在するユーザーを取得
users = User.joins(:posts)
SELECT users.*
FROM users
INNER JOIN posts ON posts.user_id = users.id;
テーブル同士を結合しているので、リレーションを持ってるテーブルのWHERE句による絞り込みもできますが、eager_load、preloadのようにキャッシュはされません。
# 特定のタイトルを持つ投稿があるユーザーを取得
users = User.joins(:posts).where(posts: { title: "Hello World" })
SELECT users.*
FROM users
INNER JOIN posts ON posts.user_id = users.id
WHERE posts.title = 'Hello World';
感想
なんとなく調べたeager_loadでしたが色んなメソッドが用意されてるんだなと思いました(小並感)
個人的には、includesメソッドが一見便利に見えますが、読む側の立場になった時に、eager_load,preloadなどを意図に合わせて使い分けた方が、ブラックボックス化されずにいいなと思いました!
では最後に宣伝です!
スペースマーケットでは、一緒にサービスを成長させていく仲間を探しています。
とりあえずどんなことをしているのか聞いてみたいという方も大歓迎です!
ご興味ありましたら是非ご覧ください!
スペースを簡単に貸し借りできるサービス「スペースマーケット」のエンジニアによる公式ブログです。 弊社採用技術スタックはこちら -> whatweuse.dev/company/spacemarket
Discussion