Вы уже переписали каталог на ElementCatalogTable::getList(), но в цикле крутите массивы $row['NAME'] и правки не доходят до витрины? Типичная ошибка на практике: setName() без save() — в БД ничего не меняется, или путаница fetch() с объектным слоем. В D7 ORM есть fetchObject() и fetchCollection() - типизированные объекты с геттерами и единым save(). Здесь разберём, когда хватит fetch() для read-only, а когда нужны объекты, createObject и wakeUpObject - и проверите цепочку на staging за один проход.
getList() возвращает DB\Result: fetch() и fetchAll() дают ассоциативные массивы, fetchObject() и fetchCollection() - EntityObject и Collection с set*() и save(). После любого set* обязателен save() и проверка Result::isSuccess(). Для новой записи - createObject(), для данных из кеша или импорта - wakeUpObject() без лишнего SELECT. Коллекцию объектов нельзя класть в ORM cache - кешируется Result выборки, не EO_Collection.
Речь про коробочную CMS 1С-Битрикс: Управление сайтом, не про CRM-портал Bitrix24. ORM (Object-Relational Mapping) - слой D7, который связывает PHP-класс *Table с таблицей MySQL. Базовый синтаксис getList (select, filter, order) разобрали в сравнении GetList и ElementTable (B60). Свою таблицу и DataManager - в гайде по своей таблице (B71). Здесь - второй уровень: objectify-слой после getList, когда массивов уже мало.
Выберите, когда нужен objectify-слой, а когда хватит fetch()
getList() всегда возвращает Result. Дальше выбор простой: читаете данные без правок - fetch() или fetchAll(); меняете поля, связи или пакетно обновляете список - fetchObject() и fetchCollection().
| Метод | Что получаете | Когда использовать |
|---|---|---|
| fetch() / fetchAll() | Ассоциативный массив полей | Отчёты, выгрузки, read-only шаблоны без save() |
| fetchObject() | EntityObject с get*() / set*() | Одна запись: правка полей, delete(), collectValues() |
| fetchCollection() | Collection объектов | Цикл по списку, fill(), групповой save(), walk() с main 26.0.0+ |
| getByPrimary()->fetchObject() | Объект по ID | Карточка элемента, когда ID уже известен |
Делайте: для витрины без записи в БД оставляйте fetch(). Не делайте: не присваивайте $row['NAME'] = 'Новое' в массиве - ORM об этом не узнает, пока не вызовете Table::update() или object->save().
Настройте fetchObject: select, set* и save() с проверкой Result
Минимальный рабочий паттерн для одной записи:
use Bitrix\Iblock\Elements\ElementCatalogTable;
$element = ElementCatalogTable::getByPrimary(42)->fetchObject();
if ($element) {
$element->setName('Новое название');
$result = $element->save();
if (!$result->isSuccess()) {
// $result->getErrorMessages()
}
}
Геттер getName() вернёт null, если поле NAME не попало в select при getList. Перед save() полезно сверить remindActualName() - до записи в БД оно показывает старое значение из базы, после save() - обновлённое. Если set* установил то же, что уже в БД, UPDATE может не сформироваться - это оптимизация ORM, не ошибка.
- Убедитесь, что в select есть все поля, которые читаете через get*().
- Вызовите set*() для нужных полей.
- Сохраните через $result = $object->save().
- Проверьте $result->isSuccess() и getErrorMessages() при сбое.
Автогенерируемые классы EO_* не проверяйте через instanceof EO_Book - после регенерации имена сменятся. Кастомизируйте через getObjectClass() в *Table и сравнивайте с Table::getObjectClass().
Делайте: оборачивайте save() в проверку Result, как при add(). Не делайте: не полагайтесь на set* без save() - изменения останутся только в памяти PHP.
Соберите fetchCollection: foreach, fill() и групповой save()
Цепочка от primary-запроса "getlist битрикс d7" к objectify:
Table::getList([...]) → fetchCollection() → foreach / walk() → set*() → collection->save()
$collection = ElementCatalogTable::getList([
'select' => ['ID', 'NAME', 'ACTIVE'],
'filter' => ['=ACTIVE' => 'Y'],
'limit' => 50,
])->fetchCollection();
foreach ($collection as $item) {
$item->setActive('N');
}
$result = $collection->save();
fetchAll() вернул бы массив строк без set* и save(). getIdList() и getNameList() на коллекции удобны для логов. Дозагрузка полей - один вызов $collection->fill(['PRICE', 'DETAIL_TEXT']), не fill() в цикле по каждому объекту: иначе N+1 SELECT вместо одного WHERE ID IN (...).
С main 26.0.0+ доступен Collection::walk() - цепочка query()->fetchCollection()->walk(...)->save() без ручного foreach. JOIN-выборки с Reference - отдельная тема в гайде по связям D7 ORM (B79); здесь важно: fetchCollection() работает и с обычным getList без runtime.
Групповой save() эффективен, когда у всех элементов меняются одни и те же поля. Если правки уникальны для каждой записи, ORM сформирует отдельные UPDATE. Для массового INSERT без событий - $collection->save(true) с $ignoreEvents=true.
Делайте: групповой fill() на коллекции. Не делайте: не кладите fetchCollection() в CPHPCache или ORM cache - подробнее в гайде по ORM cache (B85).
Подключите связанные объекты в коллекции без N+1 в цикле
Когда в map *Table настроен Reference, у объекта есть get*Collection, addTo*Collection и removeFrom*Collection. В реальном проекте часто ломается так: в foreach снова вызывают getList() на связанной таблице. Правильнее fetchCollection() → fill() на связи или runtime в том же getList - JOIN в B79.
Workflow: getList → fetchCollection() → fill при необходимости → addTo* / removeFrom* → save(). Так нет N+1 на проде.
Делайте: планируйте select и fill до цикла foreach. Не делайте: не вызывайте отдельный getList на каждую итерацию, если связь уже описана в Reference.
Создайте запись через createObject и восстановите wakeUpObject без SELECT
Новая строка без массива fields в add():
$book = BookTable::createObject();
$book->setTitle('Новая книга');
$book->setAuthorId(5);
$result = $book->save();
createObject(false) создаёт объект без дефолтов полей сущности; createObject() - с дефолтами из map. Для уже известных ID или массива полей после импорта:
$book = BookTable::wakeUpObject(17);
// или Book::wakeUp(['ID' => 17, 'TITLE' => 'Из кеша']);
$book->setTitle('Обновлённый заголовок');
$book->save();
wakeUpObject не делает SELECT - объект "оживлён" в памяти для дальнейшего set/save. События OnBeforeAdd и OnAfterAdd при save() нового объекта срабатывают так же, как при Table::add() - детали в гайде по событиям D7 ORM (B80).
Для PATCH-логики и API-сериализации:
use Bitrix\Main\ORM\Objectify\Values;
use Bitrix\Main\ORM\Fields\FieldTypeMask;
$changed = $book->collectValues(
Values::CURRENT,
FieldTypeMask::SCALAR
);
Удаление - $object->delete() с тем же Result. isChanged() и hasModified() покажут несохранённые правки. Если set*() совпал с БД, save() может вернуть успех без UPDATE - сверяйте remindActual*().
Делайте: wakeUp после кеша импорта вместо повторного getByPrimary. Не делайте: не путайте createObject (новая запись) с wakeUp (существующие данные в памяти).
Пройдите чек-лист staging перед выкладкой на prod
- Проверьте select: все поля для get*() и set*() на месте.
- Измените одну запись через fetchObject → set* → save() и сверьте админку.
- Прогоните fetchCollection с fill() - в SqlTracker один IN-запрос, не N штук.
- Убедитесь, что EO_Collection не лежит в ORM cache (B85).
Нужна помощь с миграцией legacy GetList на objectify - обсудим задачу, примеры внедрений - в портфолио.
Автор: Максим Мольков, Senior-разработчик 1С-Битрикс.
Источники: объекты ORM Bitrix Framework, коллекции ORM, выборка данных getList, урок D7: wakeUpObject.
Частые вопросы
Чем fetchObject отличается от fetch?
fetch() возвращает массив полей из Result. fetchObject() возвращает EntityObject с типизированными get*() и set*(), после которых нужен save(). Для read-only отчётов достаточно fetch(); для правок в БД берите objectify-слой.
Когда использовать fetchCollection вместо fetchAll?
fetchAll() даёт массив строк без save() на элементах. fetchCollection() даёт Collection с foreach, fill(), getIdList() и групповым save(). Выбирайте коллекцию, если планируете менять поля у нескольких записей одной цепочкой.
Как создать новый объект без Table::add(['fields' => ...])?
Вызовите Table::createObject(), заполните поля через set*() и сохраните $object->save(). Проверьте Result::isSuccess(). Это эквивалент add(), но с objectify-API и теми же событиями OnBeforeAdd.
Почему нельзя кешировать EO_Collection?
ORM cache в getList сохраняет Result выборки как набор строк, не живые объекты с методами. Коллекция из fetchCollection() держит состояние в памяти PHP; при кешировании данные залипают. Кешируйте Result или массив после fetchAll(), см. B85.
Чем save() на объекте отличается от Table::update с массивом?
Оба пишут в БД через DataManager. save() на EntityObject учитывает только изменённые поля (CURRENT vs ACTUAL), возвращает Result и запускает те же события. update() с массивом удобен без objectify; save() - когда уже работаете с fetchObject или createObject.
Зачем wakeUpObject, если есть getByPrimary?
wakeUpObject восстанавливает объект по ID или массиву полей без SELECT - после импорта, кеша или когда ID уже есть в памяти. getByPrimary всегда ходит в базу. Для экономии запросов после массовой выгрузки используйте wakeUp.
Было полезно?
Поставьте лайк, если статья помогла разобраться.
Поделиться статьёй
Отправьте тому, кому это пригодится.
Можно поделиться и фрагментом: выделите текст статьи.