Тема
Eloquent & База данных — лучшие практики
Дополняет внутренние правила
../models.md,../migrations.md,../relations.md. Здесь — общепринятые паттерны Laravel-сообщества и официальной доки.
N+1 — главный враг производительности
N+1 — это баг, не оптимизация. Защита на трёх уровнях:
php
// 1. Явный eager load в месте вызова
$posts = Post::with(['author', 'tags'])->get();
// 2. Подсчёт без загрузки
$users = User::withCount('posts')->get(); // $user->posts_count
// 3. Глобальная защита в dev (AppServiceProvider::boot)
Model::preventLazyLoading(! app()->isProduction());
Model::preventSilentlyDiscardingAttributes(! app()->isProduction());preventLazyLoading в dev/test кидает исключение при ленивой загрузке — N+1 ловится на этапе разработки, не в проде.
Вложенный и условный eager load
php
Post::with(['comments' => fn ($q) => $q->where('approved', true)->latest()])->get();
Post::with('author:id,name')->get(); // только нужные колонки
$post->loadMissing('author'); // догрузить, если ещё не загруженоЗапросы: что использовать
| Задача | Правильно | Избегать |
|---|---|---|
| Все записи с условием | ->where()->get() | Model::all() (вся таблица в память) |
| Первая запись | ->orderBy()->first() | ->first() без сортировки (недетерминирован) |
| Существование | ->exists() | ->count() > 0 (лишняя агрегация) |
| Большие выборки | ->chunkById(1000, fn) / ->lazy() | ->get() на >10k строк (OOM) |
| Массовый апдейт | ->update([...]) (один запрос) | цикл с ->save() (N запросов) |
| Агрегаты | ->sum(), ->avg() в SQL | загрузка + PHP-цикл |
chunkById, не chunk
При мутации в процессе обхода chunk() пропускает строки (сдвиг offset). chunkById() идёт по PK — безопасен:
php
User::where('active', false)->chunkById(1000, function ($users) {
foreach ($users as $user) { $user->update(['archived' => true]); }
});Транзакции
php
// Замыкание — авто-commit/rollback, retry на deadlock
DB::transaction(function () use ($data) {
$order = Order::create($data);
$order->items()->createMany($data['items']);
$this->inventory->reserve($order);
}, attempts: 3);- Несколько связанных записей → всегда транзакция (атомарность).
- Внешние вызовы (API, mail) — вне транзакции или после commit (
DB::afterCommit()/ событие). - Долгие транзакции держат блокировки — минимизировать тело.
Индексы и схема (PostgreSQL-проекты)
Детали и спец-индексы (GIN, partial, functional, covering) — в ../migrations.md. Ключевое:
- FK →
constrained()+index(), осознанныйonDelete(cascade/null/restrict). - Индексы на столбцы в WHERE / ORDER BY / JOIN.
CREATE INDEX CONCURRENTLY+$withinTransaction = falseна таблицах >1M строк.- После массового ETL →
ANALYZE table(статистика планировщика). - Кириллица +
ILIKE→ functional index наlower(col).
Модель — что внутри, что снаружи
| Внутри модели | Снаружи (Service) |
|---|---|
casts(), связи, scopes | бизнес-расчёты, формулы |
| Accessor/Mutator (нормализация) | вызовы внешних API |
Простые предикаты (isPublished()) | транзакции с несколькими моделями |
- НЕ
$withв модели (грузит всегда) →->with()в запросе. - НЕ
request()/auth()в модели (HTTP-контекст) → передавать данными. - Accessor НЕ делает запросов к БД (скрытый N+1).
Mass assignment
php
protected $fillable = ['title', 'body', 'category_id']; // whitelist — предпочтительно
// $guarded = [] — только для моделей без пользовательского ввода (сидеры, internal)preventSilentlyDiscardingAttributes (dev) кидает исключение при попытке заполнить не-fillable поле — ловит опечатки.
Scopes — переиспользуемые условия
php
// Laravel 11+ атрибут #[Scope] или классический scopeXxx
public function scopePublished(Builder $q): void
{
$q->where('status', PostStatus::Published)->where('published_at', '<=', now());
}
// Post::published()->latest()->get();Scope создавать когда условие в 2+ местах. Одноразовое — не scope.
Anti-patterns
| Паттерн | Проблема | Решение |
|---|---|---|
Model::all() | вся таблица в RAM | условия + paginate/chunk |
Цикл с save() | N запросов | bulk update()/upsert() |
DB::raw() с конкатенацией | SQL-инъекция | bindings / Eloquent |
$with в модели | грузит relation всегда | ->with() в запросе |
first() без orderBy | недетерминированный результат | orderBy()->first() |
| Транзакция с API-вызовом внутри | висящая блокировка | вынести за commit |
| Accessor с запросом к БД | скрытый N+1 | Service |