Налаштування Eloquent ORM для Laravel: від N+1 до оптимізації

Часта проблема: Eloquent працює повільно через N+1 запити, неправильні зв'язки або відсутність індексів. Це призводить до часу завантаження сторінок понад 2 секунди та високого навантаження на базу даних. Наприклад, на одному проєкті ми скоротили кількість запитів зі 150 до 5, прискоривши сторінку к

Розробка та обслуговування будь-яких видів сайтів:

Інформаційні сайти або веб-програми
Сайти візитки, landing page, корпоративні сайти, онлайн каталоги, квіз, промо-сайти, блоги, ресурси новин, інформаційні портали, форуми, агрегатори
Сайти або веб-програми електронної комерції
Інтернет-магазини, B2B-портали, маркетплейси, онлайн-обмінники, кешбек-сайти, біржі, дропшиппінг-платформи, парсери товарів
Веб-програми для управління бізнес-процесами
CRM-системи, ERP-системи, корпоративні портали, системи управління виробництвом, парсери інформації
Сайти або веб-програми електронних послуг
Дошки оголошень, онлайн-школи, онлайн-кінотеатри, конструктори сайтів, портали надання електронних послуг, відеохостинги, тематичні портали

Це лише деякі з технічних типів сайтів, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

Послуги, які ми пропонуємо
Показано 1 з 1Усі 2062 послуг
Налаштування Eloquent ORM для Laravel: від N+1 до оптимізації
Середній
~1 день

Наші компетенції:

Часті запитання

Останні роботи

  • image_website-b2b-advance_0.webp
    Розробка сайту компанії B2B ADVANCE
    1419
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1287
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    983
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1245
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    983
  • image_bitrix-bitrix-24-1c_fixper_448_0.webp
    Розробка веб-сайту для компанії ФІКСПЕР
    998

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