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:
phpuse 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 чаще всего и берут:
phpclass 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 404 | Order::findOrFail($id) или route model binding |
Подводный камень
- Массовые операции обойдут события и мутаторы.
update(),insert(),delete()на построителе не трогают модель в память — событияupdating, мутаторы и приведения типов не сработают. Если на модели есть логика «при изменении статуса отправить уведомление» — она молча не выполнится. - Ленивые загрузки копятся незаметно. Обращение к
$order->userв цикле — по одному SQL на каждый заказ (это и есть N+1). На маленькой выборке не видно, на больших страница начинает жить десятки секунд. Лечение — eager loading (with()), отдельная тема №19. create()безfillableуронит защиту. Eloquent защищает от массового присвоения: поля, не перечисленные в$fillable, игнорируются молча. Симптом «поле не сохраняется, ошибок нет» — в 90% случаев забытый$fillable.- 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();
Три вывода
- Eloquent — Active Record: модель знает, как себя сохранить; соглашения (таблица, ключ, даты) позволяют писать минимум кода, а
$fillable,$casts— то, что явно стоит указать. - Три слоя — модель, построитель запросов,
DB::table— различаются не синтаксисом, а последствиями: события моделей работают только у объектов, массовые операции их обходят. - ORM прячет SQL, но не отменяет его: за каждым вызовом — конкретный запрос, и умение его увидеть (Telescope,
toSql()) — признак зрелой работы, а не опция.
Вопрос-ловушка
Спрашивают так: «Что такое ORM и зачем он нужен?» Простой ответ «обёртка над базой» — слабый. Сильный — назвать паттерн (Active Record для Eloquent) и показать цену удобства: модель несёт в себе не только данные, но и работу с базой — за это платим производительностью на массовых операциях. Хитрый поворот: «Чем Order::where(...)->get() отличается от Order::where(...)->update([...]) кроме возвращаемого значения?» Ответ — события моделей сработают только в первом случае; кто это знает — писал в проде, а не только читал доку. И ещё один: «Можно ли в Eloquent написать любой SQL?» — почти любой, но правильный ответ включает уточнение: не всё должно быть написано на Eloquent, сложные запросы честнее писать через query builder или SQL напрямую.