Данила Пантелеев
Разработка2026-09-0610 мин чтения7

Метод fill() в ORM 1С-Битрикс: зачем он нужен и чем отличается от set()

Метод fill() в ORM 1С-Битрикс: зачем он нужен и чем отличается от set()

Метод fill в ORM 1С-Битрикс: зачем он нужен и чем отличается от set Введение: почему fill и set постоянно путают Работая с EntityObject в D7 ORM, вы постоянно меняете значения полей сущности:...

Метод fill() в ORM 1С-Битрикс: зачем он нужен и чем отличается от set()

Введение: почему fill() и set() постоянно путают

Работая с EntityObject в D7 ORM, вы постоянно меняете значения полей сущности: загрузили запись, поменяли поле, вызвали save(). На этом пути есть два метода, которые внешне выглядят одинаково – set() и fill(). Оба принимают имя поля и значение, оба «записывают» значение в объект. Но ведут они себя принципиально по-разному, и путаница между ними – источник двух зеркальных багов:

  • код с set() «лишний раз» сохраняет запись: UPDATE по полю, которое не менялось, срабатывание событий onAfterUpdate, переиндексация, лишние блокировки;
  • код с fill() молча не сохраняет нужное значение: разработчик ставит fill() перед save(), запрос выполняется без ошибок, а в базе – старые данные.

Официальная документация ограничивается формулировкой «fill не помечает поле изменённым». Это правда, но она ничего не объясняет. В статье разберём внутреннюю механику EntityObject – его хранилища значений и метод remindActual() – и из неё выведем поведение обоих методов без зубрёжки. Материал рассчитан на backend-разработчиков, пишущих кастомные модули и интеграции на Битриксе, и на тимлидов, ревьюящих чужой код.

Как EntityObject хранит значения полей

EntityObject – это не просто массив «поле → значение». Внутри объекта значения распределены по нескольким хранилищам. Ниже – упрощённая схема устройства Bitrix\Main\ORM\Objectify\EntityObject; тела методов опущены:

php
// Упрощённая схема Bitrix\Main\ORM\Objectify\EntityObject (тела методов опущены) class EntityObject { /** @var array Текущее состояние объекта – то, что вы читаете через get() */ protected $values = []; /** @var array Значения, которые объект считает фактическим состоянием записи в БД */ protected $actualValues = []; /** @var array Значения runtime- и expression-полей, которых нет в таблице */ protected $runtimeValues = []; public function remindActual($fieldName) { // Возвращает фактическое значение поля из $actualValues – // то, что объект считает состоянием записи в базе } }
  • $values – рабочее состояние: то, что возвращает get() и что вы меняете в коде.
  • $actualValues – эталон: то, что объект считает состоянием записи в базе данных.
  • $runtimeValues – значения expression-полей и вычисляемых полей, которых в таблице нет. (В реальном классе есть ещё $customValues для нестандартных полей, но к теме статьи он отношения не имеет.)

Поле считается «изменённым», когда его значение в $values расходится со значением в $actualValues. Именно эту разницу смотрит save(): при UPDATE в SQL попадают только расходящиеся поля. Свериться с эталоном можно через remindActual() – это геттер, он возвращает фактическое значение поля, то есть то, что объект считает состоянием записи в базе. А после успешного save() ORM сама переносит текущие значения в эталон: всё, что было текущим, становится фактическим.

Схема хранилищ значений EntityObject: values, actualValues и runtimeValues

Вся разница между set() и fill() – в том, какое из хранилищ меняет каждый из методов.

set(): пометить поле как изменённое

set($name, $value) пишет значение только в $values. Эталон $actualValues не трогается – возникает расхождение, и поле помечается как изменённое.

php
use Bitrix\Main\Loader; use Vendor\Module\BookTable; Loader::includeModule('vendor.module'); $book = BookTable::getByPrimary(42)->fetchObject(); $book->set('PRICE', 1990.00); // values != actualValues → поле изменено $book->save(); // Выполнится: // UPDATE b_vendor_book SET PRICE = 1990.00 WHERE ID = 42

Следствия этого простого действия шире, чем кажется:

  • save() генерирует UPDATE по помеченному полю;
  • срабатывают события onBeforeUpdate / onAfterUpdate сущности;
  • отрабатывают подписанные на события обработчики: индексация поиска, сброс кэша, бизнес-процессы, триггеры CRM – всё, что навешано на обновление этой сущности;
  • запись блокируется на время UPDATE – при конкурентном доступе это ощутимо.

Нюанс, который стоит знать точно: пометка «изменено» возникает в момент вызова set(), а финальное сравнение с фактическим значением происходит на этапе save(). Если вы вызвали set() с тем же значением, что уже в $actualValues, поле не уйдёт в UPDATE – расхождения нет. То есть безвреден не сам вызов, а только его результат: лишний set() с неизменным значением – это мёртвый код, а set() с отличающимся значением – это UPDATE, события и блокировка.

set() – правильный выбор, когда вы меняете данные и хотите зафиксировать их в базе. Это его единственное назначение.

fill(): подставить значение без пометки об изменении

fill($name, $value) пишет значение и в $values, и в $actualValues. Расхождения не возникает – поле остаётся «чистым». Для чтения через get() значение доступно, но ORM считает, что в базе уже лежит именно оно.

php
$book = BookTable::getByPrimary(42)->fetchObject(); // Поле должно быть определено в карте сущности (например, как runtime-поле), // иначе ORM бросит исключение о неизвестном поле $book->fill('PRICE_FORMATTED', '1 990 ₽'); // actualValues == values → поле чистое $book->save(); // UPDATE по полю PRICE_FORMATTED выполнен НЕ БУДЕТ.

Следствия зеркальны:

  • поле не попадает в UPDATE;
  • по этому полю не срабатывают события обновления;
  • значение, заполненное через fill(), не затрёт параллельные изменения других процессов – потому что ничего не пишется.

Типовые сценарии, где fill() – именно то, что нужно:

  • вычисляемые в коде значения для отображения: цена с форматированием, полное имя из ФИО, человекочитаемый статус;
  • подготовка данных перед выводом: подставить в объект то, что вы получили из другого источника (кэш, внешний API), не трогая базу;
  • значения по умолчанию в ещё не сохранённом объекте: заполнить поля нового объекта до того, как пользователь их увидит в форме;
  • догрузка полей коллекции – об этом отдельный раздел ниже.

fill() против set(): сравнительная таблица последствий

Последствиеset()fill()
Значение доступно через get()ДаДа
Поле попадёт в UPDATE при save()ДаНет
События onBeforeUpdate / onAfterUpdate по этому полюСработаютНе сработают
Затирание чужих параллельных измененийВозможноНевозможно
Что вернёт remindActual()Значение из БДПодставленное значение
Типовое назначениеЗапись в БДЧтение, вывод, оптимизация

Два пункта таблицы стоит раскрыть, потому что именно они бьют больнее всего на проде.

Лишний UPDATE – не бесплатен. Частая ошибка – ставить set() на вычисляемое значение при каждом сохранении «для надёжности». Каждый такой UPDATE: блокирует строку (в InnoDB – до конца транзакции), дёргает цепочку событий, запускает переиндексацию, инвалидирует кэш. На сущности с десятком подписчиков на onAfterUpdate один лишний set() превращается в десятки запросов. Кстати, та же механика «событие порождает лавину запросов» встречается и в интеграциях Битрикс24 с внешними системами – похожие грабли на стороне WordPress мы разбирали в статье про интеграцию WordPress с российскими CRM.

Затирание чужих изменений – главный риск злоупотребления set(). Сценарий: ваш процесс загрузил объект, сделал set('STATUS', 'DONE') и сохранил. Пока объект жил в памяти, другой процесс обновил PRICE. Если ваш код по какой-то причине сделал set('PRICE', ...) на устаревшее значение – вы молча откатили чужое изменение. fill() такой проблемой не страдает: он вообще не пишет в базу.

Правило выбора одной строкой: хотите сохранить значение в БД – set(); значение нужно только в объекте – fill().

fill() на коллекциях: борьба с N+1

У fill() есть второе, менее известное применение. При обходе коллекции EntityObject обращение к полю, которое не входило в select исходного запроса, вызывает ленивую подгрузку – по одному SQL-запросу на каждый элемент. Классический N+1:

php
$connection = \Bitrix\Main\Application::getConnection(); $connection->startTracker(); $books = BookTable::getList([ 'select' => ['ID', 'TITLE'], ])->fetchCollection(); foreach ($books as $book) { // AUTHOR_ID не было в select → ленивая подгрузка, +1 запрос на каждую книгу echo $book->get('AUTHOR_ID'); } echo $connection->getTracker()->getCounter(); // 1 + N запросов

fill() на коллекции догружает поле одним запросом для всех элементов:

php
$books = BookTable::getList([ 'select' => ['ID', 'TITLE'], ])->fetchCollection(); // Один запрос: SELECT ID, AUTHOR_ID FROM b_vendor_book WHERE ID IN (...) $books->fill('AUTHOR_ID'); foreach ($books as $book) { echo $book->get('AUTHOR_ID'); // уже в памяти, запросов нет }

Схема: как fill() на коллекции заменяет N+1 ленивых запросов одним запросом с IN

По числу запросов арифметика такая: обход коллекции из 100 элементов с обращением к незагруженному полю – это 1 + N = 101 запрос, а с fill() – 2 (исходный запрос + один запрос на догрузку). Проверить это на своём проекте проще всего тем же счётчиком Tracker, что в примере выше. На связях (reference-полях) эффект тот же: fill('AUTHOR') заменяет N ленивых подгрузок связанных объектов одним запросом с IN.

Честное ограничение: если вы заранее знаете, какие поля понадобятся, правильнее сразу перечислить их в select – это ноль дополнительных запросов вместо одного. fill() выигрывает, когда список нужных полей определяется динамически, после загрузки коллекции, или когда коллекцию вернул чужой код и переписать его запрос вы не можете.

Типовые ошибки и антипаттерны

1. fill() перед save() в надежде, что значение сохранится.

php
$book->fill('PRICE', 1990.00); $book->save(); // выполнится успешно, но PRICE в базе не изменится

Ошибка не падает, данные теряются молча – худший вид бага. Нужна запись – используйте set().

2. set() вычисляемых значений на каждый save.

php
// В обработчике onBeforeUpdate или перед каждым save(): $book->set('SEARCH_STRING', $book->get('TITLE') . ' ' . $book->get('AUTHOR_NAME'));

Если исходные поля не менялись и вычисленное значение совпадает с фактическим, в UPDATE оно не попадёт – но сам вызов всё равно лишний, а любое реальное расхождение даст UPDATE, события и блокировку на каждом сохранении. Правильнее: вычисляйте значение только когда изменились исходные поля, или выносите в fill() то, что сохранять вообще не нужно.

3. Чтение фактического значения после fill() – неожиданный результат.

php
$book->fill('PRICE', 1990.00); $actual = $book->remindActual('PRICE'); // вы думаете, что вернётся значение из БД // ...но fill() уже записал 1990.00 в actualValues – remindActual() вернёт его же, // а не реальное значение из таблицы

После fill() объект «верит», что в базе лежит подставленное значение. remindActual() больше не покажет реальное состояние БД – для этого нужно перезагрузить объект из базы.

4. Чек-лист для код-ревью. На что смотреть в миграциях, обработчиках событий и импортах:

  • fill() в коде, который затем вызывает save() – почти всегда баг;
  • set() внутри onAfterUpdate-обработчиков – риск рекурсии и лишних UPDATE;
  • массовые импорты, где на каждую запись вешается set() вычисляемых полей, – кандидат на замену прямым update() с массивом полей;
  • обход коллекций с обращением к полям вне select – кандидат на fill() коллекции или расширение select.

Заключение

Разница между fill() и set() – в одном флаге изменённости, который ORM вычисляет как расхождение между $values и $actualValues. set() создаёт это расхождение, fill() – нет. Но последствия каскадные: SQL-запросы, события, блокировки, конкурентные изменения, количество запросов при обходе коллекций.

Практическое правило: set() – для записи в базу, fill() – для чтения в коде и оптимизации коллекций. Плюс одна привычка: счётчик запросов (Tracker) в отладке – самый быстрый способ увидеть, где ленивая подгрузка съедает производительность и где fill() на коллекции даст мгновенный выигрыш.

Первоисточники для дальнейшего чтения: документация D7 ORM и описание класса EntityObject.

Автор: Данила Пантелеев · 2026-09-06

Нужна помощь с реализацией?

Описывайте задачу – разберём что подойдёт под ваш проект

Написать в TelegramЗаказать на Kwork

Ещё статьи

📝
Безопасность2026-09-1210 мин

Безопасность плагинов WordPress: аудит, обновления и защита от уязвимостей

Безопасность плагинов WordPress: аудит, обновления и защита от уязвимостей Ядро WordPress обновляется централизованно и регулярно – и взламывают сайты на нём в подавляющем большинстве случаев не...

Читать
WordPress + Telegram: 3 готовых сценария автоматизации заявок, уведомлений и поддержки с рабочим кодом
Плагины2026-09-1016 мин

WordPress + Telegram: 3 готовых сценария автоматизации заявок, уведомлений и поддержки с рабочим кодом

WordPress + Telegram: 3 готовых сценария автоматизации заявок, уведомлений и поддержки с рабочим кодом Введение: почему заявки из WordPress должны приходить в Telegram Классическая ситуация: клиент...

Читать
Кэширование в Django: Redis, Memcached и кэширование фрагментов шаблонов
Django2026-07-2414 мин

Кэширование в Django: Redis, Memcached и кэширование фрагментов шаблонов

Кэширование в Django: Redis, Memcached и кэширование фрагментов шаблонов Введение: зачем Django-проекту кэширование Симптомы знакомы каждому, кто выкатывал Django под реальную нагрузку: страница...

Читать