Объекты ORM - выборка объектами, ленивая загрузка, сохранение
Работаем с данными объектами, а не массивами: выборка объектами и коллекциями, подгрузка полей, сохранение изменений и удаление связанных записей.
Механика
У слоя данных два стиля работы, и оба поддерживаются одинаково хорошо. Массивы удобны для вывода и отчётов, объекты - для прикладной логики, где запись меняют и сохраняют несколькими шагами.
Объект знает свои поля, свои связи и накопленные изменения. Правки копятся в нём и уходят в базу одним вызовом сохранения, а не отдельным запросом на каждое изменённое поле.
Ленивой подгрузки полей здесь нет, и это главное расхождение с привычками из других сред. Поле, не перечисленное в выборке, вернёт пустое значение или ошибку при обращении к обязательному полю.
Недостающие поля догружают явно отдельным вызовом. Он делает один запрос и заполняет объект, поэтому в цикле по сотне записей его не место: там поля перечисляют сразу в выборке.
Коллекция - это набор объектов с обходом и общими операциями. Она удобна там, где дальше идёт работа со связанными записями, а не простой вывод строк на страницу.
Для запросов с группировкой объекты не годятся вовсе. Агрегаты не ложатся на модель записи, и такие выборки читают массивами, как обычный результат запроса.
Служебные классы объектов в коде не упоминают напрямую. Объект создают методом сущности, а свой класс объекта регистрируют в самой сущности, если он нужен.
Удаление записи не трогает связанные данные автоматически. Каскада здесь нет: связанные записи удаляют своим кодом или обработчиком события удаления сущности.
Объекты хорошо ложатся на бизнес-логику с несколькими шагами и проверками. Заказ, его позиции и отметки о доставке удобно держать связанными объектами, а не разрозненными массивами, которые приходится согласовывать руками.
Шаги
- Решить по задаче, что удобнее: массивы для вывода или объекты для логики.
- Перечислить в выборке все поля и связи, которые понадобятся дальше по коду.
- Догружать недостающие поля отдельным вызовом, но никогда не внутри цикла.
- Проверять результат сохранения и факт изменения строки, а не только отсутствие ошибок.
- Удалять связанные записи явно, потому что каскадного удаления в слое данных нет.
Код
Читаем одну запись объектом:
$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# без этого подсказки методов объекта в редакторе устареваютОграничения
Объекты дороже массивов по памяти и по времени сборки. На выборке в десятки тысяч строк разница заметна, и отчёты по-прежнему собирают массивами.
Ограничение числа строк работает по строкам запроса, а не по числу записей. При связи «один ко многим» пять строк выборки могут оказаться одной записью с неполным набором связанных данных.
Несколько связей в одной выборке дают произведение строк. Три отношения по десятку записей превращаются в тысячи строк результата, поэтому связи разбирают отдельными запросами.
Кэшированная выборка возвращает другой тип результата. Проверки на конкретный класс результата ломаются при попадании в кэш, и полагаться на них не стоит.
Массовое обновление объектами всегда дороже обновления по фильтру. Тысяча объектов - это тысяча наборов полей в памяти, тогда как одно обновление по условию делает ту же работу единственным запросом к базе.
Типичные проблемы
Обращение к полю возвращает пустое значение.
Поле не перечислено в выборке, а ленивой подгрузки в слое данных нет. Поле добавляют в выборку или догружают явным вызовом.
Сохранение прошло, а данные не изменились.
Проверялось только отсутствие ошибок, а не число изменённых строк. Факт изменения показывает счётчик затронутых строк результата.
Количество в связке всегда нулевое.
Запись идёт через отношение многие-ко-многим, которое своих полей не пишет. Для данных связи заводят отдельную сущность-посредник.
После удаления записи остались связанные строки.
Каскадного удаления в слое данных нет вовсе. Связанные записи удаляют своим кодом или обработчиком события удаления.
Выборка с группировкой возвращает странные объекты.
Запрос с агрегатами читается объектами, а модель записи для этого не годится. Агрегаты читают массивами обычного результата.
Частые вопросы
Что выбрать: массивы или объекты?
Для вывода списков и отчётов дешевле массивы, для прикладной логики удобнее объекты. В одном проекте спокойно живут оба стиля, но в одном участке кода выбирают что-то одно.
Почему нет ленивой загрузки полей?
Она давала бы скрытые запросы в цикле и непредсказуемую нагрузку. Здесь поля перечисляют явно, а недостающие догружают отдельным вызовом.
Как получить связанные записи?
Перечислить связь в выборке или обратиться к ней у объекта после подгрузки. Несколько связей сразу лучше разбирать отдельными запросами.
Можно ли добавить свои методы объекту?
Да, своим классом объекта, зарегистрированным в сущности. Служебные классы, созданные платформой, в коде напрямую не упоминают.
Зачем нужны аннотации?
Они дают редактору подсказки по методам объектов и коллекций. После правки карты полей их перегенерируют, иначе подсказки устаревают.
Смежное
- Свои таблицы на ORM - оглавление подтемы
- Своя таблица на ORM: сущность, запросы, изменение структуры - как заводят сущность
- Связи между таблицами ORM: ссылки, выборка, удаление - устройство связей подробнее
- Выборка ORM возвращает не то: разбор причин - когда результат неожиданный
- Транзакции: когда нужны и как не сломать данные - сохранение нескольких сущностей разом
- Своя сущность от таблицы до админки: список, форма, права - интерфейс поверх сущности
- Инфоблок через ORM: API_CODE, класс элементов, свойства - те же объекты у элементов инфоблока
- Ядро D7: ORM - устройство слоя данных целиком