Настройка Eloquent ORM для Laravel: от N+1 до оптимизации

Ваш Laravel-проект тормозит из-за неоптимальных запросов Eloquent? Мы настраиваем ORM под ключ: устраняем N+1, выстраиваем правильные связи и индексы. Наша команда берёт на себя весь цикл — от аудита до внедрения, обеспечивая стабильную работу и последующую поддержку.

Разработка и обслуживание любых видов сайтов:

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

Это лишь некоторые из технических типов сайтов, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента

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

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

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

Последние работы

  • Разработка сайта компании B2B ADVANCE
    Разработка сайта компании B2B ADVANCE
    1542
  • Разработка веб-приложения для компании FEEDME
    Разработка веб-приложения для компании FEEDME
    1394
  • Разработка веб-сайта для компании БЕЛФИНГРУПП
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    1081
  • Разработка интернет магазина для компании FURNORO
    Разработка интернет магазина для компании FURNORO
    1369
  • Разработка веб-приложения для компании Enviok
    Разработка веб-приложения для компании Enviok
    1142
  • Разработка веб-сайта для компании ФИКСПЕР
    Разработка веб-сайта для компании ФИКСПЕР
    1060