Настройка Eloquent ORM для Laravel веб-приложения
Eloquent — активная запись поверх PDO с поддержкой PostgreSQL, MySQL, SQLite и SQL Server. В Laravel 10/11 он уже встроен, но правильная конфигурация — не только .env. Рассматриваем весь путь: от конфига подключения до advanced-техник.
Конфигурация подключения
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
Для endpoint каталога с пагинацией:
$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.
Для глобального предотвращения N+1 в тестовой среде:
// AppServiceProvider::boot()
if (app()->isLocal()) {
Model::preventLazyLoading();
}
Это выбросит 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.
Repository pattern (опционально)
Eloquent — active record, и прямое использование в контроллерах — норма для небольших проектов. Для сложной бизнес-логики разделяем:
<?php
namespace App\Repositories;
use App\Models\Product;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
class ProductRepository
{
public function getPublishedPaginated(
int $categoryId,
int $perPage = 24,
): LengthAwarePaginator {
return Product::query()
->published()
->inCategory($categoryId)
->with(['category', 'tags'])
->orderByDesc('created_at')
->paginate($perPage);
}
}
Сроки
Настройка Eloquent для нового Laravel-проекта: 1 день (модели, миграции, seeders, базовые тесты). Аудит и рефакторинг существующего проекта с N+1 проблемами, неоптимальными индексами и отсутствующими FK-ограничениями: 1–2 дня.







