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







