
Метод 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; тела методов опущены:
// Упрощённая схема 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 сама переносит текущие значения в эталон: всё, что было текущим, становится фактическим.

Вся разница между set() и fill() – в том, какое из хранилищ меняет каждый из методов.
set(): пометить поле как изменённое
set($name, $value) пишет значение только в $values. Эталон $actualValues не трогается – возникает расхождение, и поле помечается как изменённое.
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 считает, что в базе уже лежит именно оно.
$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:
$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() на коллекции догружает поле одним запросом для всех элементов:
$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'); // уже в памяти, запросов нет
}
По числу запросов арифметика такая: обход коллекции из 100 элементов с обращением к незагруженному полю – это 1 + N = 101 запрос, а с fill() – 2 (исходный запрос + один запрос на догрузку). Проверить это на своём проекте проще всего тем же счётчиком Tracker, что в примере выше. На связях (reference-полях) эффект тот же: fill('AUTHOR') заменяет N ленивых подгрузок связанных объектов одним запросом с IN.
Честное ограничение: если вы заранее знаете, какие поля понадобятся, правильнее сразу перечислить их в select – это ноль дополнительных запросов вместо одного. fill() выигрывает, когда список нужных полей определяется динамически, после загрузки коллекции, или когда коллекцию вернул чужой код и переписать его запрос вы не можете.
Типовые ошибки и антипаттерны
1. fill() перед save() в надежде, что значение сохранится.
$book->fill('PRICE', 1990.00);
$book->save(); // выполнится успешно, но PRICE в базе не изменитсяОшибка не падает, данные теряются молча – худший вид бага. Нужна запись – используйте set().
2. set() вычисляемых значений на каждый save.
// В обработчике onBeforeUpdate или перед каждым save():
$book->set('SEARCH_STRING', $book->get('TITLE') . ' ' . $book->get('AUTHOR_NAME'));Если исходные поля не менялись и вычисленное значение совпадает с фактическим, в UPDATE оно не попадёт – но сам вызов всё равно лишний, а любое реальное расхождение даст UPDATE, события и блокировку на каждом сохранении. Правильнее: вычисляйте значение только когда изменились исходные поля, или выносите в fill() то, что сохранять вообще не нужно.
3. Чтение фактического значения после fill() – неожиданный результат.
$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.


