Eloquent: ORM в Laravel простыми словами

Eloquent — это ORM (Object-Relational Mapping): слой, который превращает таблицы базы данных в PHP-классы, а строки — в объекты. Каждая таблица — модель, каждая запись — объект этой модели, а связи между таблицами становятся свойствами. Вместо ручных SQL-запросов вы работаете с объектами: $order->user->email, $order->save(), Order::where('status', 'paid')->get(). Понимать Eloquent нужно глубже, чем «писал $order->save()»: именно на нём чаще всего ломают прод — N+1 запросов, случайное обновление полей, потерянные данные при массовых операциях. Кто не понимает, что Eloquent делает под капотом, рано или поздно получает и то, и другое.


Зачем это нужно

Боль из практики: в проекте делали массовое обновление товаров через модель:

php
// ПЛОХО: массовое обновление через модель — медленно и не так, как думает разработчик
$products = Product::where('category_id', 5)->get();   // выбрали 10 000 строк

foreach ($products as $product) {
    $product->update(['price' => $product->price * 1.1]);  // 10 000 отдельных UPDATE
}

Выглядело понятно и «в духе Eloquent». В реальности: 10 000 запросов к базе, десять секунд на простую операцию и растянутые транзакции. Человек писал код, не понимая разницы между работой с объектами (модель → события → сохранение построчно) и работой с наборами строк (query builder → один UPDATE на всё):

php
// ХОРОШО: один запрос на весь набор, когда событиям моделей не место
Product::where('category_id', 5)->update(['price' => DB::raw('price * 1.1')]);

Ошибка не в Eloquent, а в незнании, что под каждой строкой кода — конкретный SQL. ORM — не замена знанию базы, а удобная надстройка над ним.

Как это работает

Eloquent реализует паттерн Active Record: модель сама знает, как себя сохранить, найти и удалить. Соглашения: таблица — множественное число от имени класса (Order → orders), первичный ключ — id, поля с датами — created_at/updated_at:

php
use Illuminate\Database\Eloquent\Model;

class Order extends Model          // таблица orders, ключ id
{
    // Eloquent сам знает таблицу и ключ — по соглашению.
    // Нестандартные случаи — явно:
    protected $table = 'shop_orders';
    protected $primaryKey = 'order_id';

    protected $fillable = ['status', 'total', 'user_id'];  // защита от массового присвоения
    protected $casts = [                                            // типы полей
        'total' => 'decimal:2',
        'paid_at' => 'datetime',
    ];
}

Три способа работы с данными — и у каждого своя роль:

php
// 1. Объект (модель): один заказ, события, mutators, сохранение
$order = Order::find(1);              // SELECT * FROM orders WHERE id = 1 LIMIT 1
$order->status = 'shipped';
$order->save();                       // UPDATE orders SET ... WHERE id = 1

// 2. Построитель запросов (query builder через модель): наборы строк, SQL-условия
$paid = Order::where('status', 'paid')
    ->where('total', '>', 1000)
    ->orderBy('created_at', 'desc')
    ->get();                          // SELECT * FROM orders WHERE ... ORDER BY ...

// 3. Сырой query builder: без моделей, максимально близко к SQL
$rows = DB::table('orders')
    ->join('users', 'users.id', '=', 'orders.user_id')
    ->select('users.name', DB::raw('SUM(orders.total) as revenue'))
    ->groupBy('users.name')
    ->get();

Ключевое различие, которое ловят на практике:

  • Order::where(...)->get() возвращает коллекцию моделей — у каждой сработают события, мутаторы, доступна $item->user через связи.
  • Order::where(...)->update([...]) — один SQL-запрос, но событий моделей и мутаторов не будет: Eloquent не достаёт записи в память, а просто выполняет UPDATE.
  • DB::table('orders') — вообще без моделей: чистые строки-объекты, максимально быстро, без магии.

Связи — то, ради чего Eloquent чаще всего и берут:

php
class Order extends Model
{
    public function user()
    {
        return $this->belongsTo(User::class);   // orders.user_id → users.id
    }

    public function items()
    {
        return $this->hasMany(OrderItem::class); // один заказ — много позиций
    }
}

// Использование — как свойства, без SQL:
echo $order->user->email;          // belongsTo: SQL под капотом выполнен один раз
foreach ($order->items as $item) { // hasMany: коллекция OrderItem
    echo $item->price;
}

Как это выглядит в Laravel

  • Модели живут в app/Models/, генерируются: php artisan make:model Order -m (флаг -m заодно создаст миграцию, -f — фабрику, -c — контроллер).
  • php artisan tinker — REPL Laravel: в нём удобно пробовать запросы Eloquent живьём и смотреть, какой SQL получится через DB::enableQueryLog().
  • Telescope / Debugbar — показывают реальные SQL-запросы на странице; первый инструмент, когда «страница тормозит».
  • toSql() на построителе — быстрый способ увидеть SQL без выполнения.

Плохо / Хорошо

ПлохоХорошо
Цикл foreach с save() по 10 000 строкОдин update() по набору — если события не нужны
Order::all() в контроллере ради двух полей->select(...) и пагинация
Логика SQL-фильтрации в PHP-циклеУсловия в query builder — фильтрует база
Eloquent везде, даже для тяжёлых отчётовОтчёты — через query builder или SQL
Order::find($id) с последующим if (!$order) return 404Order::findOrFail($id) или route model binding

Подводный камень

  1. Массовые операции обойдут события и мутаторы. update(), insert(), delete() на построителе не трогают модель в память — события updating, мутаторы и приведения типов не сработают. Если на модели есть логика «при изменении статуса отправить уведомление» — она молча не выполнится.
  2. Ленивые загрузки копятся незаметно. Обращение к $order->user в цикле — по одному SQL на каждый заказ (это и есть N+1). На маленькой выборке не видно, на больших страница начинает жить десятки секунд. Лечение — eager loading (with()), отдельная тема №19.
  3. create() без fillable уронит защиту. Eloquent защищает от массового присвоения: поля, не перечисленные в $fillable, игнорируются молча. Симптом «поле не сохраняется, ошибок нет» — в 90% случаев забытый $fillable.
  4. Eloquent — не замена SQL. Тяжёлые отчёты, оконные функции, HAVING, подзапросы на Eloquent превращаются в нечитаемый монстр с DB::raw() внутри. ORM — для объектов и связей; сложная аналитика — честный SQL или query builder.

Мини-шпаргалка

php
// Объекты: один — находи, меняй, сохраняй
$order = Order::findOrFail(1);
$order->update(['status' => 'paid']);

// Наборы: фильтрация в базе, не в PHP
$recent = Order::where('status', 'paid')
    ->where('created_at', '>=', now()->subDays(7))
    ->latest()
    ->limit(10)
    ->get();

// Связи
$order->user->email;             // belongsTo
$order->items()->count();        // hasMany

// Посмотреть SQL без выполнения
Order::where('status', 'paid')->toSql();

Три вывода

  1. Eloquent — Active Record: модель знает, как себя сохранить; соглашения (таблица, ключ, даты) позволяют писать минимум кода, а $fillable, $casts — то, что явно стоит указать.
  2. Три слоя — модель, построитель запросов, DB::table — различаются не синтаксисом, а последствиями: события моделей работают только у объектов, массовые операции их обходят.
  3. ORM прячет SQL, но не отменяет его: за каждым вызовом — конкретный запрос, и умение его увидеть (Telescope, toSql()) — признак зрелой работы, а не опция.

Вопрос-ловушка

Спрашивают так: «Что такое ORM и зачем он нужен?» Простой ответ «обёртка над базой» — слабый. Сильный — назвать паттерн (Active Record для Eloquent) и показать цену удобства: модель несёт в себе не только данные, но и работу с базой — за это платим производительностью на массовых операциях. Хитрый поворот: «Чем Order::where(...)->get() отличается от Order::where(...)->update([...]) кроме возвращаемого значения?» Ответ — события моделей сработают только в первом случае; кто это знает — писал в проде, а не только читал доку. И ещё один: «Можно ли в Eloquent написать любой SQL?» — почти любой, но правильный ответ включает уточнение: не всё должно быть написано на Eloquent, сложные запросы честнее писать через query builder или SQL напрямую.