Урок 02

Eager loading: with(), load()

Что такое eager loading

Eager loading — предзагрузка связей до обхода коллекции.

Lazy loading — по обращению (N+1).

Eager — все связи одним запросом.

Правило: eager — всегда, если знаешь, что будешь использовать связь.

with() — базовый

$posts = Post::with('author')->get();

Что происходит:

  1. SELECT * FROM posts.
  2. SELECT * FROM users WHERE id IN (...).

Правило: with — первое, что добавлять к запросу.

Много связей

$posts = Post::with(['author', 'comments', 'tags'])->get();

Что даёт: по одному запросу на связь.

Вложенные

$posts = Post::with('comments.author')->get();

Что происходит:

  1. posts.
  2. comments.
  3. users авторов комментариев.

Правило: точка — глубина.

Вложенные на много уровней

$posts = Post::with('comments.author.company.department')->get();

Что важно: каждый уровень — отдельный запрос.

Правило: глубина > 3 — подозрительно, лучше вручную.

Множественные at same level

$posts = Post::with([
    'comments.author',
    'comments.reactions',
    'comments.replies',
])->get();

Что даёт: общие родители — один запрос.

Правило: группировать по префиксу.

with() с колонками

$posts = Post::with('author:id,name,email')->get();

Что важно: всегда включать id.

Правило: id — обязателен в select.

with() с условиями

$posts = Post::with(['comments' => function ($q) {
    $q->where('approved', true)->orderBy('created_at', 'desc');
}])->get();

Что даёт: предзагрузка с фильтром.

Правило: with с closure — явные условия.

with() с отношениями

$posts = Post::with(['author' => function ($q) {
    $q->where('active', true);
}])->get();

Что важно: where работает внутри подзапроса.

Nested with (Laravel 11+)

$posts = Post::query()
    ->with([
        'author' => ['company', 'department'],
    ])
    ->get();

Что даёт: компактнее.

withCount()

$posts = Post::withCount('comments')->get();

$post->comments_count;

Что даёт: счётчик без N+1.

Правило: count() на связи → withCount.

withCount с фильтром

$posts = Post::withCount(['comments' => function ($q) {
    $q->where('approved', true);
}])->get();

$post->comments_count;

Что даёт: счётчик с условием.

withSum, withAvg, withMin, withMax

$posts = Post::withSum('views', 'count')
             ->withAvg('ratings', 'value')
             ->withMin('views', 'count')
             ->withMax('views', 'count')
             ->get();

Что даёт: агрегаты без N+1.

Правило: агрегаты — всегда через withX.

load()

Когда: модель уже получена.

$post = Post::find(1);
$post->load('author');
$post->load(['comments', 'tags']);

Правило: load — после выборки.

loadMissing()

$post->loadMissing('author');

Что делает: грузит только если не загружено.

Правило: loadMissing безопаснее load.

loadCount()

$post->loadCount('comments');

Что даёт: счётчик после выборки.

loadMorph()

Полиморфные:

$activities = Activity::get();
$activities->loadMorph('subject', [
    Post::class => ['author'],
    Comment::class => ['post'],
]);

Что даёт: eager для полиморфных связей.

Многоуровневый load

$post->load([
    'comments.author',
    'comments.reactions',
]);

Что важно: глубоко после уже полученной модели.

whenLoaded()

В Blade:

@if($post->relationLoaded('author'))
    {{ $post->author->name }}
@endif

В PHP:

if ($post->relationLoaded('author')) {
    //
}

Правило: relationLoaded — быстрая проверка.

Условный eager

$posts = Post::query()
    ->when($request->boolean('with_author'), fn ($q) => $q->with('author'))
    ->when($request->boolean('with_comments'), fn ($q) => $q->with('comments'))
    ->get();

Правило: API-параметры → условные eager.

Ленивое ограничение

$posts = Post::with(['comments' => fn ($q) => $q->limit(5)])->get();

⚠️ limit не работает на eager-подзапросах в Laravel.

Решение: hasMany()->latestOfMany() или отдельный запрос.

Eager для мофрованных

$comments = Comment::with('commentable')->get();

foreach ($comments as $comment) {
    echo $comment->commentable->title;   // 0 SQL
}

Правило: полиморфные — та же механика.

belongsToMany

$posts = Post::with('tags')->get();

Что происходит:

  1. posts.
  2. post_tag (pivot).
  3. tags.

Правило: 3 запроса вместо N+1.

belongsToMany с pivot

$posts = Post::with('tags')->get();

foreach ($posts as $post) {
    foreach ($post->tags as $tag) {
        echo $tag->pivot->created_at;
    }
}

Что даёт: pivot без доп. запросов.

hasManyThrough

$countries = Country::with('posts.comments')->get();

Что важно: работает как обычная связь.

Ошибки eager

1. Select без id:

Post::with('author:id,name')->get();   // связь пустая

Решение: author:id,name,id не работает — включить id первым.

Правильно:

Post::with('author:id,name')->get();   // id включён

2. where на главной вместо with:

Post::with('comments')->where('comments.approved', true)->get();   // ошибка

Решение: условие внутри closure.

3. $with в модели:

protected $with = ['author'];   // грузит всегда

Решение: убрать.

Комбинация с chunk

Eager + chunk:

Post::with('author')->chunk(500, function ($posts) {
    foreach ($posts as $post) {
        // обработать
    }
});

Что даёт: память + связи.

Правило: с chunk — всегда with.

Профилирование

Debugbar:

  • Queries вкладка.
  • Группировка по SQL.

Telescope:

  • Requests → вкладка Queries.

Pulse:

  • Slow queries.

Правило: проверить количество SQL после with.

Практика

1. Найдите все ->count() на связи.

2. Замените на withCount().

3. Найдите циклы с $model->relation.

4. Добавьте with().

5. Проверьте select в with — есть id?

6. Замените load на loadMissing.

7. Уберите $with из моделей.

8. Прогоните Telescope → Queries.

9. Прогоните тесты.

10. Замерьте p95.

Частые ошибки

1. select без id:

Связь пустая

Решение: id всегда.

2. $with в модели:

Лишние запросы

Решение: убрать.

3. where на главной:

Ошибка SQL

Решение: closure.

4. load вместо loadMissing:

Двойная загрузка

Решение: loadMissing.

5. Глубина > 3:

Много запросов

Решение: вручную.

6. limit в with:

Не работает

Решение: latestOfMany.

7. Игнорировать pivot:

N+1 на pivot

Решение: withPivot.

8. Eager для всех:

Медленнее

Решение: только нужное.

Итог

  • with() — основное.
  • Dot-нотация — вложенные.
  • select — всегда с id.
  • withCount — счётчики.
  • withSum / withAvg — агрегаты.
  • load() — после выборки.
  • loadMissing — безопасно.
  • whenLoaded — проверка.
  • Условия — внутри closure.
  • Профилировать через Telescope.

Дальше — оптимизация Eloquent: select(), chunk(), cursor().