LLaravel Bootcamp

Day 4EloquentとリレーションとN+1問題

Eager Loading(事前ロード)

with で関連データをまとめて先読みし、N+1を根本から解決する。遅延ロードとの違いを押さえる。

遅延ロードと事前ロードの違い

リレーションの読み込み方には2種類あります。「遅延ロード(Lazy Loading)」はアクセスされた瞬間に都度SQLを出す方式、「(事前ロード)」は必要な関連を最初にまとめて読み込んでおく方式です。N+1を防ぐには、後者を意識的に選びます。

遅延ロード(既定・N+1の温床)

アクセス時に都度SQL。ループで回すと件数分に増える。

PHP
$posts = Post::all();            // 著者は読み込まれていない
foreach ($posts as $post) {
    echo $post->user->name;      // ここで初めて、1件ずつSQLが飛ぶ
}

事前ロード(with で先読み)

必要な関連を最初にまとめて取得。ループ内では追加SQLが出ない。

PHP
$posts = Post::with('user')->get(); // 著者もまとめて先読み
foreach ($posts as $post) {
    echo $post->user->name;         // 追加SQLなし
}

なぜ事前ロードで速くなるのか

事前ロードは「必要な著者IDをまとめて、WHERE id IN (...) の1本で取得する」からです。DBへの往復回数が『件数分』から『1回』に減るのが本質です。1本ごとのSQLが速くても、往復の回数が多いこと自体が遅さの原因なので、回数を減らすことが効きます。

ネストした関連や複数関連の先読み

PHP
// 複数の関連を同時に先読み
$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」を使います。

PHP
// 各投稿の「コメント件数」を1回のクエリでまとめて取得
$posts = Post::withCount('comments')->get();

foreach ($posts as $post) {
    // comments_count というプロパティが自動で用意される
    echo $post->comments_count;
}
// コメント本文を全件読み込まずに、件数だけ取れる
withCount で件数を効率的に取得する

落とし穴: 何でもwithすればいいわけではない

使わない関連まで先読みすると、逆に無駄なデータをメモリに載せてしまいます。事前ロードは「そのページで実際に使う関連だけ」を対象にするのが原則です。件数しか要らないならwithCount、本文も要るならwith、と目的で使い分けます。N+1を恐れて過剰にwithするのも別の無駄を生みます。

面接ではこう言う

「N+1はwithによるEager Loadingで解決します。関連をWHERE IN でまとめて1本にすることで、DBへの往復回数を件数分から1回に減らせます。件数だけ欲しい場面ではwithCountを使い、本文を読み込まずに済ませます。使う関連だけを先読みする、という点も意識しています」と説明できます。

読んでも腑に落ちない所は、AIに噛み砕いてもらいましょう。

理解度チェック

答えられれば、面接で説明できる状態に一歩近づきます。

Q1

with('user') による事前ロードが速い理由として、最も適切なものはどれか。

Q2

各投稿の「コメント件数」だけを効率よく表示したい。最も適切な方法はどれか。

Q3

遅延ロード(Lazy Loading)と事前ロード(Eager Loading)の違いを、N+1との関係を含めて説明せよ。

記述式です。まず自分の言葉で答えを考えてから、模範解答を開いて自己採点してください。