Разработка · 7 мин чтения

Eloquent ORM для новичков: основные паттерны работы с БД

Eloquent ORM - первое, что я подключаю, когда сажусь за бэкенд на Laravel: встроенная в фреймворк ORM по паттерну Active Record, где каждая таблица БД превращается в PHP-класс-модель, а строки - в объекты. Вместо сырых SQL-запросов вы пишете Order::where('status', 'paid')->get() и получаете коллекцию готовых объектов. За несколько лет я собрал на Eloquent десятки проектов - от админок интернет-магазинов до бэкенда с приёмом платежей через T‑Bank и расчётом доставки СДЭК - и почти все паттерны, которые реально нужны новичку, укладываются в один разбор.

Зачем нужен Eloquent ORM и где я его применяю

Eloquent закрывает три задачи разом: убирает ручной SQL из повседневного кода, даёт единый слой доступа к данным и связывает таблицы через отношения. На практике разница чувствуется сразу. Когда я делал бэкенд для интернет-магазина, заказ тянул за собой позиции, клиента, статус оплаты T‑Bank и трек-номер СДЭК. На чистом SQL это превратилось бы в кашу из JOIN-ов, которую страшно трогать через полгода. С Eloquent модель Order сама знает про свои связи, и код читается как бизнес-логика, а не как дамп базы.

Я беру Eloquent почти в каждом Laravel-проекте: CRM-панели, API для мобильных приложений, миграции магазинов с WooCommerce на кастомный бэкенд. Единственное место, где я осознанно ухожу в сторону - тяжёлые аналитические выборки на сотни тысяч строк, но об этом ниже. Для 90% задач ORM экономит часы и снижает шанс словить SQL-инъекцию, потому что параметры экранируются автоматически.

Модели Eloquent: с чего начинается работа с БД

Модель - это класс, который наследуется от Model и по умолчанию привязан к таблице по соглашению об именовании. Таблица orders ищется автоматически для модели Order, id считается первичным ключом, а поля created_at и updated_at Laravel заполняет сам. Меньше конфигурации - меньше ошибок.

<?php

namespace AppModels;

use IlluminateDatabaseEloquentModel;

class Order extends Model
{
    protected $fillable = ['user_id', 'total', 'status'];

    protected $casts = [
        'is_paid' => 'boolean',
        'meta'    => 'array',
    ];

    public function items()
    {
        return $this->hasMany(OrderItem::class);
    }
}

Два момента, на которых спотыкаются новички. Первый - $fillable. Без него Laravel защищает от массового заполнения и кидает MassAssignmentException, как только вы попробуете Order::create($request->all()). Перечисляйте в $fillable только те поля, которые клиент вправе передавать: сумму заказа туда класть нельзя, иначе покупатель отправит total = 1 и оплатит рубль вместо пяти тысяч. Это не теория - такую дыру я находил в чужом коде на аудите.

Второй - $casts. Он превращает строку из базы в нужный тип прямо при чтении. Поле meta с JSON станет обычным PHP-массивом, а is_paid - булевым значением. Без каста вы будете постоянно писать json_decode руками и ловить баги на сравнении '0' == false.

CRUD через запросы Eloquent

Базовый набор операций - создание, чтение, обновление, удаление - выглядит почти как псевдокод. Именно из-за этого Eloquent так легко даётся тем, кто пришёл с фронтенда или из Tilda-скриптов и раньше не писал SQL руками.

// создать
$order = Order::create([
    'user_id' => 5,
    'total'   => 4900,
    'status'  => 'new',
]);

// прочитать
$order  = Order::find(1);
$fresh  = Order::where('status', 'new')->latest()->get();
$paid   = Order::where('total', '>', 1000)->count();

// обновить
$order->update(['status' => 'paid']);

// удалить
$order->delete();


Отдельно выделю updateOrCreate и firstOrCreate - на практике они спасают чаще всего. Когда прилетает вебхук об оплате от эквайринга T‑Bank, я обрабатываю его через updateOrCreate по order_id. Платёжный шлюз имеет право прислать один и тот же вебхук дважды, и без идемпотентности в базе появился бы дубль платежа. С этим методом повторный вызов просто обновит существующую запись.

Payment::updateOrCreate(
    ['external_id' => $webhook['PaymentId']],
    ['status' => $webhook['Status'], 'amount' => $webhook['Amount']]
);

Ещё один совет новичку: не тяните всю таблицу через all(), чтобы потом фильтровать в PHP. Пусть фильтрует база - where, orderBy, limit отдаются на сторону СУБД, и это на порядок быстрее.

Бесплатный материал

🎁 Полезный скрипт в подарок

Подпишитесь на Telegram - пришлю готовый скрипт по этой теме.

Без спама. Отписка в 1 клик.

Связи между моделями: relationships в Laravel ORM

Связи - то, ради чего Eloquent вообще стоит учить. Вы описываете отношение один раз в модели, а дальше обращаетесь к нему как к обычному свойству: $order->items, $order->user. Laravel сам построит нужный запрос.

Тип связи Методы Пример из практики
Один ко многим hasMany / belongsTo Заказ и его позиции
Один к одному hasOne / belongsTo Пользователь и профиль
Многие ко многим belongsToMany Товары и категории
Полиморфная morphMany Комментарии к заказам и товарам

Самая частая пара - hasMany и обратный ей belongsTo. Заказ имеет много позиций, позиция принадлежит заказу. Для связки товаров с категориями беру belongsToMany с промежуточной таблицей category_product: один товар живёт в нескольких категориях, категория содержит много товаров. Через связь можно сразу и записывать - $product->categories()->attach($categoryId) добавит строку в сводную таблицу без единой строчки SQL.

Полиморфные связи на реальном кейсе

Когда в магазине понадобились отзывы и на товары, и на заказы, я не стал плодить две таблицы product_reviews и order_reviews. Одна таблица reviews с полями reviewable_id и reviewable_type через morphMany закрыла обе сущности. Позже туда же легко подключились отзывы на службу доставки - код связи не менялся.

N+1 и eager loading - где новички теряют производительность

Проблема N+1 - главная ловушка Eloquent, и я вижу её в каждом втором проекте, который прихожу чинить. Выглядит она безобидно:

// плохо: 1 запрос на заказы + по запросу на каждого клиента
$orders = Order::all();
foreach ($orders as $order) {
    echo $order->user->name;
}

// хорошо: 2 запроса на всё
$orders = Order::with('user')->get();

В первом варианте на 200 заказов уйдёт 201 запрос к базе: один на список и по одному на клиента в каждой итерации. Именно так у меня однажды дашборд открывался четыре секунды. Добавил with('user') - стало два запроса и меньше 200 мс. Это и есть eager loading, жадная загрузка связей заранее.

Полезные соседи: with(['items', 'user']) грузит сразу несколько связей, load() подтягивает связь к уже полученной модели, а withCount('items') добавляет количество позиций без загрузки самих строк. Ловить N+1 удобно через Laravel Debugbar или Telescope - они показывают число запросов на страницу, и лишние сотни видно сразу. Такие проверенные заготовки запросов я держу под рукой и переиспользую между проектами, чтобы не собирать одно и то же заново - часть из них выложена как готовые скрипты и сниппеты для Laravel.

Eloquent или Query Builder: что выбрать

Внутри Laravel живёт ещё и Query Builder (DB::table()) - более низкоуровневый слой без моделей. Я использую оба, но по разным поводам.

Критерий Eloquent Query Builder
Стиль Объекты и модели SQL-подобные вызовы
Связи Из коробки Ручные JOIN
Большие выборки Медленнее из-за гидрации объектов Быстрее, отдаёт массивы
Когда беру 90% CRUD и бизнес-логики Отчёты, массовые апдейты, агрегации

Правило простое: пишете бизнес-логику с моделями и связями - Eloquent. Гоняете отчёт на полмиллиона строк или делаете массовый update без событий модели - Query Builder, потому что он не создаёт объект на каждую строку и экономит память. Для выгрузки данных наружу, например в автоматизацию на n8n, я тоже нередко беру DB::table() с явным select нужных колонок - так быстрее и предсказуемее.

Если проект перерастает пару моделей и вам нужен продуманный слой данных с оплатой, доставкой и интеграциями, разработку API и бэкенда на Laravel я беру от 100 000 ₽, а разовую консультацию по архитектуре БД - от 3 000 ₽. Для сравнения: в студиях аналогичный бэкенд на рынке нередко оценивают в 200 000-400 000 ₽, но это рыночный ориентир, а не мой прайс.

API и серверная часть под SPA

API / Бэкенд

от 100 000 ₽

Подробнее →

Частые вопросы

Чем Eloquent отличается от Query Builder?

Eloquent работает с моделями-объектами и знает про связи между таблицами, а Query Builder - это низкоуровневый конструктор SQL, который возвращает простые массивы. Eloquent удобнее для бизнес-логики и CRUD, Query Builder быстрее на больших выборках и массовых операциях. В одном проекте нормально использовать оба.

Нужно ли знать SQL, если есть Eloquent?

Базовый SQL знать обязательно. Eloquent прячет синтаксис, но не саму логику: без понимания индексов, JOIN и проблемы N+1 вы рано или поздно упрётесь в медленные запросы и не поймёте почему. Я советую параллельно смотреть, какой SQL генерирует ORM, - это делается через toSql() или Debugbar.

Что такое N+1 и как его поймать?

N+1 - это когда к одному запросу за списком добавляется по отдельному запросу на каждую связанную запись, и вместо двух обращений к базе получается двести. Лечится жадной загрузкой через with(). Поймать проще всего Laravel Debugbar или Telescope: они показывают число SQL-запросов на страницу.

Подходит ли Eloquent для больших нагрузок?

Да, до определённого масштаба. При грамотных индексах, eager loading и кэшировании Eloquent тянет высоконагруженные проекты. На тяжёлой аналитике и выборках в сотни тысяч строк я переключаюсь на Query Builder или chunk-обработку, чтобы не держать миллионы объектов в памяти. Смешанный подход закрывает почти любые требования.

Есть задача?

Обсудим в мессенджере

Расскажите, что нужно сделать — отвечу в течение 4 часов в рабочее время. Первая консультация бесплатно.

Самозанятый Калинкин Н. А. · работаю с физлицами и юрлицами

Продолжая пользование настоящим сайтом Вы выражаете своё согласие на обработку Ваших персональных данных (файлов куки) с использованием Yandex.Metrika.
Понятно