Eloquent это реализация Active Record в Laravel. Почти на каждом собесе спрашивают про N+1, жадную загрузку и как не положить продакшен миграцией.
Связи
class Post extends Model
{
public function author(): BelongsTo { return $this->belongsTo(User::class); }
public function comments(): HasMany { return $this->hasMany(Comment::class); }
public function tags(): BelongsToMany { return $this->belongsToMany(Tag::class)->withTimestamps(); }
}
$post->comments; // коллекция, загружается один раз и кешируется
$post->comments()->where('approved', true)->get(); // построитель запроса
Есть ещё hasOne, hasManyThrough и полиморфные morphTo/morphMany.
N+1
$posts = Post::all();
foreach ($posts as $post) {
echo $post->author->name; // запрос на каждый пост
}
Лечение:
Post::with(['author', 'comments.user'])->get(): по запросу на каждую связь (WHERE id IN (...));$posts->load('author')для уже полученной коллекции;withCount('comments'),withSum('items', 'price'),withExists: агрегаты подзапросом, без загрузки связей;Model::preventLazyLoading(! app()->isProduction()): ленивая загрузка бросает исключение в разработке.
При ограничении колонок в with('author:id,name') обязательно берите ключи, иначе связи не сопоставятся.
Большие объёмы
chunkById(1000, fn($orders) => ...)иlazyById(): порции по первичному ключу, надёжно при изменении записей.cursor(): одна модель в памяти, но один долгий запрос.- Массовые изменения одним запросом:
Order::where(...)->update([...]),->delete(). Они не вызывают события моделей. upsertиinsertдля пакетной вставки.
Транзакции и блокировки
DB::transaction(function () use ($id, $amount) {
$account = Account::lockForUpdate()->findOrFail($id);
if ($account->balance < $amount) {
throw new InsufficientFunds();
}
$account->decrement('balance', $amount);
});
Без lockForUpdate два параллельных запроса прочитают одинаковый баланс и спишут дважды. Альтернатива: атомарный UPDATE ... WHERE balance >= ? с проверкой числа изменённых строк.
Массовое присваивание
$fillable (разрешённые поля) или $guarded. В create() передают только валидированные данные: $request->validated().
Миграции
Schema::create('orders', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->string('status')->index();
$table->timestamps();
});
- Применённые миграции не правят, изменения делают новыми.
- В продакшене
migrate --forceпри деплое,--pretendпоказывает SQL. - Изменения схемы совместимы со старым и новым кодом одновременно. Переименование колонки делают в несколько релизов: добавить новую, писать в обе, перенести данные, переключить чтение, удалить старую (expand and contract).
- Индексы на больших таблицах создаются с блокировками: планируйте окно или используйте возможности СУБД (
CREATE INDEX CONCURRENTLYв PostgreSQL).
Active Record и Data Mapper
Eloquent смешивает данные и сохранение в одном классе: быстро писать, сложнее тестировать без базы. Doctrine (Data Mapper) отделяет сущности от EntityManager. Для сложного домена держите модели Eloquent тонкими, логику выносите в сервисы.
Частые ошибки
- Ленивая загрузка в циклах и шаблонах.
Model::all()на больших таблицах.->count()на связи в цикле вместоwithCount.- Изменение колонки за один релиз без совместимости.
- Отправка заданий внутри транзакции без
afterCommit.
Что спросят на собесе
- Что такое N+1 и как его найти и исправить в Laravel?
- Чем
$user->ordersотличается от$user->orders()? - Как обработать миллион записей?
- Как предотвратить двойное списание при параллельных запросах?
- Как выкатывать миграции без простоя?