🤔

【Ruby on Rails】eager_loadって何だろう

2024/11/29に公開

はじめに

こんにちは!気づいたら今年ももう終わりますね!
社会人になってから時間が高速で進んでる気がしてるのは僕だけでしょうか?

時を高速で進めてる人

という事で最近バックエンドのコードを触る機会が増えてきたのですが、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などを意図に合わせて使い分けた方が、ブラックボックス化されずにいいなと思いました!

では最後に宣伝です!
スペースマーケットでは、一緒にサービスを成長させていく仲間を探しています。
とりあえずどんなことをしているのか聞いてみたいという方も大歓迎です!
ご興味ありましたら是非ご覧ください!
https://spacemarket.co.jp/recruit/engineer/

スペースマーケット Engineer Blog

Discussion