Day 4・EloquentとリレーションとN+1問題
Eager Loading(事前ロード)
with で関連データをまとめて先読みし、N+1を根本から解決する。遅延ロードとの違いを押さえる。
遅延ロードと事前ロードの違い
リレーションの読み込み方には2種類あります。「遅延ロード(Lazy Loading)」はアクセスされた瞬間に都度SQLを出す方式、「(事前ロード)」は必要な関連を最初にまとめて読み込んでおく方式です。N+1を防ぐには、後者を意識的に選びます。
遅延ロード(既定・N+1の温床)
アクセス時に都度SQL。ループで回すと件数分に増える。
$posts = Post::all(); // 著者は読み込まれていない
foreach ($posts as $post) {
echo $post->user->name; // ここで初めて、1件ずつSQLが飛ぶ
}事前ロード(with で先読み)
必要な関連を最初にまとめて取得。ループ内では追加SQLが出ない。
$posts = Post::with('user')->get(); // 著者もまとめて先読み
foreach ($posts as $post) {
echo $post->user->name; // 追加SQLなし
}なぜ事前ロードで速くなるのか
事前ロードは「必要な著者IDをまとめて、WHERE id IN (...) の1本で取得する」からです。DBへの往復回数が『件数分』から『1回』に減るのが本質です。1本ごとのSQLが速くても、往復の回数が多いこと自体が遅さの原因なので、回数を減らすことが効きます。
ネストした関連や複数関連の先読み
// 複数の関連を同時に先読み
$posts = Post::with(['user', 'tags'])->get();
// ネスト: 投稿の著者の、さらにプロフィールまで先読み
$posts = Post::with('user.profile')->get();
// 先読みする関連にも条件を付けられる
$posts = Post::with(['comments' => function ($query) {
$query->where('approved', true);
}])->get();withCount: 件数だけ欲しいとき
「各投稿のコメント本文は要らないが、コメント件数だけ表示したい」という場面はよくあります。関連そのものを全部読み込むと無駄なので、件数だけを効率的に取る「withCount」を使います。
// 各投稿の「コメント件数」を1回のクエリでまとめて取得
$posts = Post::withCount('comments')->get();
foreach ($posts as $post) {
// comments_count というプロパティが自動で用意される
echo $post->comments_count;
}
// コメント本文を全件読み込まずに、件数だけ取れる落とし穴: 何でもwithすればいいわけではない
使わない関連まで先読みすると、逆に無駄なデータをメモリに載せてしまいます。事前ロードは「そのページで実際に使う関連だけ」を対象にするのが原則です。件数しか要らないならwithCount、本文も要るならwith、と目的で使い分けます。N+1を恐れて過剰にwithするのも別の無駄を生みます。
面接ではこう言う
「N+1はwithによるEager Loadingで解決します。関連をWHERE IN でまとめて1本にすることで、DBへの往復回数を件数分から1回に減らせます。件数だけ欲しい場面ではwithCountを使い、本文を読み込まずに済ませます。使う関連だけを先読みする、という点も意識しています」と説明できます。
読んでも腑に落ちない所は、AIに噛み砕いてもらいましょう。
理解度チェック
答えられれば、面接で説明できる状態に一歩近づきます。
with('user') による事前ロードが速い理由として、最も適切なものはどれか。
各投稿の「コメント件数」だけを効率よく表示したい。最も適切な方法はどれか。
遅延ロード(Lazy Loading)と事前ロード(Eager Loading)の違いを、N+1との関係を含めて説明せよ。
記述式です。まず自分の言葉で答えを考えてから、模範解答を開いて自己採点してください。