Eager loading: with(), load()
Что такое eager loading
Eager loading — предзагрузка связей до обхода коллекции.
Lazy loading — по обращению (N+1).
Eager — все связи одним запросом.
Правило: eager — всегда, если знаешь, что будешь использовать связь.
with() — базовый
$posts = Post::with('author')->get();
Что происходит:
- SELECT * FROM posts.
- SELECT * FROM users WHERE id IN (...).
Правило: with — первое, что добавлять к запросу.
Много связей
$posts = Post::with(['author', 'comments', 'tags'])->get();
Что даёт: по одному запросу на связь.
Вложенные
$posts = Post::with('comments.author')->get();
Что происходит:
- posts.
- comments.
- 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();
Что происходит:
- posts.
- post_tag (pivot).
- 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().