Частая проблема: Eloquent работает медленно из-за N+1 запросов, неправильных связей или отсутствия индексов. Это приводит к времени загрузки страниц более 2 секунд и высокой нагрузке на базу данных. Например, на одном проекте мы сократили количество запросов со 150 до 5, ускорив страницу каталога с 3.5 с до 0.3 с — в 10 раз быстрее. Мы настраиваем Eloquent ORM под ключ, исправляя эти узкие места. Наша команда имеет 5+ лет опыта с Laravel и помогла оптимизировать более 50 проектов, снижая количество запросов до 90% и ускоряя ответы в 3–5 раз. В этой статье разберём полный цикл настройки: от конфига до advanced техник, таких как мутаторы и глобальные скоупы. Свяжитесь с нами для оценки вашего проекта — мы проанализируем текущую БД и предложим план оптимизации.
Laravel documentation recommends using preventLazyLoading in development.
Конфигурация подключения
config/database.php — изменяем только критичные параметры, остальное из .env:
'pgsql' => [
'driver' => 'pgsql',
'url' => env('DATABASE_URL'),
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '5432'),
'database' => env('DB_DATABASE', 'forge'),
'username' => env('DB_USERNAME', 'forge'),
'password' => env('DB_PASSWORD', ''),
'charset' => 'utf8',
'prefix' => '',
'search_path' => 'public',
'sslmode' => env('DB_SSLMODE', 'prefer'),
'options' => [
PDO::ATTR_PERSISTENT => env('DB_PERSISTENT', false),
PDO::ATTR_TIMEOUT => 10,
],
],
Для production за reverse proxy включайте ATTR_PERSISTENT = false — persistent connections конфликтуют с pgBouncer в transaction mode.
Модель
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Eloquent\Builder;
class Product extends Model
{
use SoftDeletes;
protected $table = 'products';
protected $fillable = [
'title', 'slug', 'price', 'status', 'category_id',
];
protected $casts = [
'price' => 'decimal:2',
'meta' => 'array', // JSON колонка
'published_at' => 'datetime',
'is_featured' => 'boolean',
];
// Scope для фильтрации
public function scopePublished(Builder $query): Builder
{
return $query->where('status', 'published');
}
public function scopeInCategory(Builder $query, int $categoryId): Builder
{
return $query->where('category_id', $categoryId);
}
// Computed attribute (Laravel 9+)
protected function priceWithTax(): Attribute
{
return Attribute::make(
get: fn () => round($this->price * 1.2, 2),
);
}
// Отношения
public function category(): \Illuminate\Database\Eloquent\Relations\BelongsTo
{
return $this->belongsTo(Category::class);
}
public function tags(): \Illuminate\Database\Eloquent\Relations\BelongsToMany
{
return $this->belongsToMany(Tag::class, 'product_tags')
->withTimestamps();
}
public function images(): \Illuminate\Database\Eloquent\Relations\HasMany
{
return $this->hasMany(ProductImage::class)->orderBy('sort_order');
}
}
Миграции
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('products', function (Blueprint $table) {
$table->id();
$table->string('title', 500);
$table->string('slug', 520)->unique();
$table->foreignId('category_id')->constrained()->restrictOnDelete();
$table->decimal('price', 12, 2);
$table->string('status', 20)->default('draft');
$table->jsonb('meta')->nullable();
$table->boolean('is_featured')->default(false);
$table->timestamp('published_at')->nullable();
$table->timestamps();
$table->softDeletes();
$table->index(['status', 'created_at']);
$table->index(['category_id', 'status']);
});
}
public function down(): void
{
Schema::dropIfExists('products');
}
};
foreignId('category_id')->constrained() — создаёт FK с именем products_category_id_foreign, ссылающимся на categories.id. restrictOnDelete() предпочтительнее cascadeOnDelete() для продуктового каталога: не дадим случайно удалить категорию с товарами.
Как избежать N+1 запросов в Eloquent?
Для endpoint каталога с пагинацией используйте eager loading:
$products = Product::query()
->published()
->with(['category', 'tags', 'images' => fn ($q) => $q->limit(1)])
->whereHas('category', fn ($q) => $q->where('is_active', true))
->orderBy('created_at', 'desc')
->paginate(24);
with() выполнит три дополнительных запроса вместо N+1. Ограничение limit(1) на изображения — частый трюк для preview. Правильно настроенный eager loading ускоряет страницу в 10 раз по сравнению с ленивой загрузкой при большом количестве связанных моделей. Для глобального предотвращения N+1 в тестовой среде добавьте Model::preventLazyLoading() в AppServiceProvider — это выбросит LazyLoadingViolationException при первой же ленивой загрузке и заставит исправить запрос до production.
Транзакции
use Illuminate\Support\Facades\DB;
DB::transaction(function () use ($orderId, $items) {
$order = Order::findOrFail($orderId);
$order->update(['status' => 'processing']);
foreach ($items as $item) {
Product::where('id', $item['product_id'])
->decrement('stock', $item['quantity']);
}
$order->payments()->create([
'amount' => $order->total,
'gateway' => 'stripe',
]);
});
При исключении — автоматический rollback. Третий аргумент DB::transaction($fn, 3) — количество повторных попыток при deadlock.
Почему важно использовать restrictOnDelete вместо cascadeOnDelete?
В продуктовом каталоге случайное удаление категории не должно уничтожать товары. cascadeOnDelete приводит к потере данных. restrictOnDelete блокирует удаление, пока есть связанные записи. Это одна из частых ошибок в миграциях, которую мы исправляем при аудите. В одном из проектов клиент потерял 15 000 товаров из-за cascade — после нашего аудита мы восстановили данные и настроили правильные ограничения.
Мутаторы и аксессоры
Мутаторы и аксессоры позволяют преобразовывать значения атрибутов при записи и чтении. Например, с помощью Attribute::make можно создать виртуальное поле price_with_tax, которое вычисляется на лету. Это упрощает работу с данными и уменьшает дублирование логики. Вот ещё один пример: мутатор для хеширования пароля при сохранении:
protected function password(): Attribute
{
return Attribute::make(
set: fn (string $value) => bcrypt($value),
);
}
Типичные ошибки при настройке Eloquent
| Ошибка | Последствие | Решение |
|---|---|---|
| Отсутствие индексов на столбцах WHERE/ORDER BY | Полное сканирование таблицы, медленные запросы | Добавить индексы: $table->index('status') |
| Использование cascadeOnDelete для важных данных | Потеря данных при удалении родителя | Использовать restrictOnDelete или softDeletes |
| Ленивая загрузка отношений в цикле | N+1 запросов, рост времени выполнения | Заменить на eager loading через with() |
| Хранение JSON в текстовом поле | Невозможность использовать индексы и операции JSON | Использовать тип jsonb (PostgreSQL) |
Что входит в настройку Eloquent под ключ
| Этап | Описание |
|---|---|
| Аудит | Проверка текущих моделей, связей и индексов, выявление узких мест |
| Проектирование | Проектирование схемы БД с учётом бизнес-логики и нагрузок |
| Миграции | Написание миграций с индексами, внешними ключами и ограничениями |
| Seeders | Создание тестовых данных для разработки и тестирования |
| Оптимизация | Устранение N+1, настройка жадной загрузки, добавление индексов |
Также мы предоставляем документацию API и обучение команды работе с Eloquent. Гарантируем качество выполненных работ и даём рекомендации по дальнейшему развитию. Закажите настройку Eloquent под ключ — ваша БД будет работать быстро и стабильно. Экономия на инфраструктуре после оптимизации достигает значительных сумм.
Сроки
Настройка Eloquent для нового Laravel-проекта: от 1 дня (модели, миграции, seeders, базовые тесты). Аудит и рефакторинг существующего проекта с N+1 проблемами, неоптимальными индексами и отсутствующими FK-ограничениями: 1–2 дня. Получите консультацию по вашему проекту — мы оценим объём работ бесплатно.
Эффективная настройка Eloquent ORM — это не только производительность, но и поддерживаемость кода. Eloquent ORM является стандартом для Laravel, и правильная его конфигурация важна для масштабируемых приложений. Свяжитесь с нами, чтобы начать оптимизацию.







