Часта проблема: 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, і правильна його конфігурація важлива для масштабованих застосунків. Зв'яжіться з нами, щоб почати оптимізацію.







