Перейти к содержимому

Объекты ORM - выборка объектами, ленивая загрузка, сохранение

Работаем с данными объектами, а не массивами: выборка объектами и коллекциями, подгрузка полей, сохранение изменений и удаление связанных записей.

Механика

У слоя данных два стиля работы, и оба поддерживаются одинаково хорошо. Массивы удобны для вывода и отчётов, объекты - для прикладной логики, где запись меняют и сохраняют несколькими шагами.

Объект знает свои поля, свои связи и накопленные изменения. Правки копятся в нём и уходят в базу одним вызовом сохранения, а не отдельным запросом на каждое изменённое поле.

Ленивой подгрузки полей здесь нет, и это главное расхождение с привычками из других сред. Поле, не перечисленное в выборке, вернёт пустое значение или ошибку при обращении к обязательному полю.

Недостающие поля догружают явно отдельным вызовом. Он делает один запрос и заполняет объект, поэтому в цикле по сотне записей его не место: там поля перечисляют сразу в выборке.

Коллекция - это набор объектов с обходом и общими операциями. Она удобна там, где дальше идёт работа со связанными записями, а не простой вывод строк на страницу.

Для запросов с группировкой объекты не годятся вовсе. Агрегаты не ложатся на модель записи, и такие выборки читают массивами, как обычный результат запроса.

Служебные классы объектов в коде не упоминают напрямую. Объект создают методом сущности, а свой класс объекта регистрируют в самой сущности, если он нужен.

Удаление записи не трогает связанные данные автоматически. Каскада здесь нет: связанные записи удаляют своим кодом или обработчиком события удаления сущности.

Объекты хорошо ложатся на бизнес-логику с несколькими шагами и проверками. Заказ, его позиции и отметки о доставке удобно держать связанными объектами, а не разрозненными массивами, которые приходится согласовывать руками.

Шаги

  1. Решить по задаче, что удобнее: массивы для вывода или объекты для логики.
  2. Перечислить в выборке все поля и связи, которые понадобятся дальше по коду.
  3. Догружать недостающие поля отдельным вызовом, но никогда не внутри цикла.
  4. Проверять результат сохранения и факт изменения строки, а не только отсутствие ошибок.
  5. Удалять связанные записи явно, потому что каскадного удаления в слое данных нет.

Код

Читаем одну запись объектом:

$book = \Vendor\Module\BookTable::getByPrimary($id, [
'select' => ['ID', 'TITLE', 'PUBLISH_DATE'], // перечисляем всё, что нужно дальше
])->fetchObject();
printf("%s от %s\n", $book->getTitle(), $book->getPublishDate()->format('d.m.Y'));

Методы доступа к полям объект создаёт по карте сущности сам. Имя поля из карты превращается в привычное имя метода, и опечатка в нём становится ошибкой сразу, а не пустым значением в массиве.

Догружаем недостающее поле:

$book->fill(['DESCRIPTION']); // один запрос за недостающими полями
echo $book->getDescription();
// в цикле по списку записей так делать нельзя: получится запрос на каждую строку

Выбор между перечислением полей и догрузкой решается заранее. Один объект можно дополнить по ходу дела, а список из сотни записей обязан приходить сразу со всеми нужными полями.

Читаем список коллекцией:

$books = \Vendor\Module\BookTable::getList([
'select' => ['ID', 'TITLE', 'AUTHOR_ID'],
'filter' => ['=ACTIVE' => 'Y'],
'order' => ['PUBLISH_DATE' => 'DESC'],
])->fetchCollection();
foreach ($books as $book) { echo $book->getTitle(), "\n"; }

Коллекция обходится как обычный набор и знает свои элементы. Для простого вывода таблицы она избыточна: там дешевле массив, который не строит объект на каждую строку результата.

Создаём и сохраняем запись:

$book = \Vendor\Module\BookTable::createObject();
$book->setTitle('Новая книга')->setActive('Y');
$result = $book->save();
if (!$result->isSuccess()) {
print_r($result->getErrorMessages()); // без проверки ошибки уходят в предупреждения
}

Проверка результата обязательна при любой записи. Непроверенный отказ валидации превращается в предупреждение уровня языка, которое на боевом сайте никто не увидит.

Отличаем успех от фактического изменения:

$result = \Vendor\Module\BookTable::update($id, ['TITLE' => $title]);
printf("успех=%d строк изменено=%d\n", $result->isSuccess(), $result->getAffectedRowsCount());
// успех означает отсутствие ошибок, а не то, что строка действительно поменялась

Успешный результат и изменённая строка - разные вещи. Обновление несуществующей записи или запись прежнего значения проходят успешно, и число изменённых строк здесь единственный честный ответ.

Храним данные самой связи отдельной сущностью:

// у связи есть своё поле количества, поэтому нужна сущность-посредник
$link = \Vendor\Module\OrderBookTable::createObject();
$link->setOrderId($orderId)->setBookId($bookId)->setQuantity(3);
$link->save();
// добавление через связь многие-ко-многим запишет только значение по умолчанию

Связь с собственными полями оформляют отдельной сущностью. Через готовое отношение многие-ко-многим такие поля не записываются, и количество в связке навсегда останется нулевым.

Удаляем запись вместе со связанными:

foreach (\Vendor\Module\OrderBookTable::getList(['filter' => ['=BOOK_ID' => $id]]) as $row) {
\Vendor\Module\OrderBookTable::delete($row['ID']); // каскада нет, чистим сами
}
\Vendor\Module\BookTable::delete($id);

Сохраняем изменения всей коллекции:

$books = \Vendor\Module\BookTable::getList([
'select' => ['ID', 'ACTIVE'], 'filter' => ['=AUTHOR_ID' => $authorId],
])->fetchCollection();
foreach ($books as $book) { $book->setActive('N'); } // правки копятся в объектах
$books->save(); // запись одним проходом по набору
// сохранение коллекции не заменяет обновление по фильтру на больших объёмах

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

Аннотации нужны редактору, а не самой платформе. Без них подсказки по методам объектов устаревают после каждой правки карты полей, и разработчик начинает угадывать имена вместо того, чтобы выбирать их из списка.

Обновляем аннотации после правки карты:

Окно терминала
php bitrix/modules/main/cli.php orm:annotate --module vendor.module
# без этого подсказки методов объекта в редакторе устаревают

Ограничения

Объекты дороже массивов по памяти и по времени сборки. На выборке в десятки тысяч строк разница заметна, и отчёты по-прежнему собирают массивами.

Ограничение числа строк работает по строкам запроса, а не по числу записей. При связи «один ко многим» пять строк выборки могут оказаться одной записью с неполным набором связанных данных.

Несколько связей в одной выборке дают произведение строк. Три отношения по десятку записей превращаются в тысячи строк результата, поэтому связи разбирают отдельными запросами.

Кэшированная выборка возвращает другой тип результата. Проверки на конкретный класс результата ломаются при попадании в кэш, и полагаться на них не стоит.

Массовое обновление объектами всегда дороже обновления по фильтру. Тысяча объектов - это тысяча наборов полей в памяти, тогда как одно обновление по условию делает ту же работу единственным запросом к базе.

Типичные проблемы

Обращение к полю возвращает пустое значение.

Поле не перечислено в выборке, а ленивой подгрузки в слое данных нет. Поле добавляют в выборку или догружают явным вызовом.

Сохранение прошло, а данные не изменились.

Проверялось только отсутствие ошибок, а не число изменённых строк. Факт изменения показывает счётчик затронутых строк результата.

Количество в связке всегда нулевое.

Запись идёт через отношение многие-ко-многим, которое своих полей не пишет. Для данных связи заводят отдельную сущность-посредник.

После удаления записи остались связанные строки.

Каскадного удаления в слое данных нет вовсе. Связанные записи удаляют своим кодом или обработчиком события удаления.

Выборка с группировкой возвращает странные объекты.

Запрос с агрегатами читается объектами, а модель записи для этого не годится. Агрегаты читают массивами обычного результата.

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

Что выбрать: массивы или объекты?

Для вывода списков и отчётов дешевле массивы, для прикладной логики удобнее объекты. В одном проекте спокойно живут оба стиля, но в одном участке кода выбирают что-то одно.

Почему нет ленивой загрузки полей?

Она давала бы скрытые запросы в цикле и непредсказуемую нагрузку. Здесь поля перечисляют явно, а недостающие догружают отдельным вызовом.

Как получить связанные записи?

Перечислить связь в выборке или обратиться к ней у объекта после подгрузки. Несколько связей сразу лучше разбирать отдельными запросами.

Можно ли добавить свои методы объекту?

Да, своим классом объекта, зарегистрированным в сущности. Служебные классы, созданные платформой, в коде напрямую не упоминают.

Зачем нужны аннотации?

Они дают редактору подсказки по методам объектов и коллекций. После правки карты полей их перегенерируют, иначе подсказки устаревают.

Смежное

Первоисточник