Почему в типовых конфигурациях 1С используется Набор записей вместо Менеджера записи для изменения одной строки?

Программист 1С v8.3 (Управляемые формы) 1С:Управление торговлей Управленческий учет Торговля и дистрибуция
← На главную

При изучении кода современных типовых конфигураций (например, «1С:Управление торговлей 11.5» или «1С:ERP») многие разработчики сталкиваются с неочевидным архитектурным решением. Рассмотрим конкретный пример: в регистре сведений РаспределениеЗапасов, который заменил собой сразу несколько регистров остатков, запись движений происходит весьма специфичным образом. Разработчики 1С используют НаборЗаписей в цикле: устанавливают отбор на одну строку, добавляют запись, записывают набор, а затем очищают его.

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

Сравнение подходов: Менеджер записи против Набора записей

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


ТекЗапись = РегистрыСведений.РаспределениеЗапасов.СоздатьМенеджерЗаписи();       
ТекЗапись.Состояние = Перечисления.РаспределениеЗапасовСостояния.ОстатокНаСкладе;   
ТекЗапись.Номенклатура = ВыборкаСтрока.Номенклатура;
ТекЗапись.Характеристика = ВыборкаСтрока.Характеристика;
ТекЗапись.Склад   = ВиртуальныйСклад;    
ТекЗапись.Прочитать(); 
                
ТекЗапись.Состояние = Перечисления.РаспределениеЗапасовСостояния.ОстатокНаСкладе;   
ТекЗапись.ТипЗаписиРаспределенияЗапасов = 1;
ТекЗапись.Номенклатура = ВыборкаСтрока.Номенклатура;
ТекЗапись.Характеристика = ВыборкаСтрока.Характеристика;
ТекЗапись.Склад   = ВиртуальныйСклад;
ТекЗапись.ВНаличии = ВыборкаСтрока.МегапрайсКоличество;  
ТекЗапись.Свободно = ВыборкаСтрока.МегапрайсКоличество;
ТекЗапись.Запас = ВыборкаСтрока.МегапрайсКоличество;
ТекЗапись.Записать();

А теперь проанализируем код, который используется разработчиками фирмы «1С» в общих модулях (например, в модуле распределения запасов):


Набор = РегистрыСведений.РаспределениеЗапасов.СоздатьНаборЗаписей();
Набор.Отбор.ТипЗаписиРаспределенияЗапасов.Установить(1);
Набор.Отбор.Состояние.Установить(Перечисления.РаспределениеЗапасовСостояния.ОстатокНаСкладе);
    
Пока Выборка.Следующий() Цикл
        
    Набор.Отбор.Номенклатура.Установить(Выборка.Номенклатура);
    Набор.Отбор.Характеристика.Установить(Выборка.Характеристика);
    Набор.Отбор.Склад.Установить(Выборка.Склад);
    Набор.Отбор.Назначение.Установить(Выборка.Назначение);
    Если Выборка.Запас <> 0
            Или Выборка.Резерв <> 0
            Или Выборка.Свободно <> 0
            Или Выборка.ВНаличии <> 0 Тогда
                
        НоваяСтрока = Набор.Добавить();
        ЗаполнитьЗначенияСвойств(НоваяСтрока, Выборка);
                
    КонецЕсли;
    Набор.Записать();
    Набор.Очистить();
        
КонецЦикла;

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

1. Исключение лишнего обращения к базе данных (Слепая запись)

Обратите внимание на метод Прочитать() в первом примере. Менеджер записи чаще всего используется по схеме: «создать менеджер — заполнить ключи — прочитать — изменить — записать». Вызов метода чтения — это выполнение полноценного SQL-запроса SELECT к базе данных.

В высоконагруженных системах (таких как распределение остатков) нам не нужно знать предыдущие значения ресурсов, так как мы полностью перезаписываем их новыми (рассчитанными) данными. Использование объекта НаборЗаписей с установленными отборами позволяет выполнить так называемую «слепую запись» (Blind Write). Платформа генерирует оптимизированный SQL-запрос, который просто заменяет данные в таблице БД по ключу, не тратя драгоценное время на предварительное чтение и пересылку данных с сервера СУБД на сервер 1С. Это делает код существенно быстрее.

2. Атомарность операции: универсальное удаление и запись

Главное отличие набора записей при работе с отборами заключается в том, как платформа обрабатывает метод Записать(). При вызове этого метода платформа в рамках одной транзакции сначала удаляет все существующие в базе записи, соответствующие текущему отбору, а затем записывает новые строки, добавленные в набор.

Посмотрим на условие в типовом коде. Если расчетные ресурсы (Запас, Резерв, Свободно и т.д.) равны нулю, условие не выполняется, и строка в набор не добавляется. Но метод Набор.Записать() все равно вызывается! Что произойдет? Платформа запишет пустой набор, что фактически означает удаление записи из базы данных по данному ключу.

Если бы мы использовали МенеджерЗаписи, нам пришлось бы писать дополнительную логику:

  1. Проверять, есть ли данные для записи.
  2. Если данные есть — выполнять Записать().
  3. Если данных (остатков) нет — выполнять отдельный метод Удалить().

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

3. Минимизация дедлоков и управление блокировками

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

Чтобы избежать дедлока при использовании менеджера записи, перед вызовом Прочитать() необходимо явно устанавливать объект БлокировкаДанных (управляемую блокировку). Набор записей дает более прозрачный и безопасный контроль. Оперируя сразу всей совокупностью данных по конкретному ключу (отфильтрованному в наборе), система снижает риск возникновения эффекта «фантомного чтения» и конфликтов эскалации блокировок.

4. Почему запись идет в цикле по одной строке?

Регистр «Распределение запасов» заменяет классические регистры накопления. Любое проведение заказа, приходного ордера или реализации меняет эти данные. Возникает вопрос: почему не добавить все строки выборки в один большой набор и не записать его один раз за пределами цикла?

Ответ кроется в длительности транзакционных блокировок. Если мы соберем массив из 1000 товаров и выполним запись единым огромным набором, СУБД заблокирует все 1000 позиций. Другие документы, в которых встречается хотя бы один товар из этой тысячи, встанут в очередь и будут ждать (ожидание на блокировке).

Записывая изменения короткими итерациями по одной строке (в рамках транзакции), система осуществляет микро-блокировки конкретных записей на доли миллисекунд. Это компромисс между пакетной обработкой и обеспечением максимальной параллельности работы пользователей. Таким образом другие сеансы могут беспрепятственно проводить документы с другими товарами.

Подведение итогов

Рассмотренный нами подход (создание набора записей, установка отборов в цикле, добавление строки, запись и очистка) не является недочетом разработчиков, а представляет собой продуманный паттерн высокопроизводительной разработки (Best Practice) для 1С:Предприятия.

Резюмируем преимущества Набора записей в данном сценарии:

  1. Отсутствует лишний запрос SELECT к базе данных, запись идет «вслепую», что работает быстрее.
  2. Не нужно писать ветвления с вызовом Удалить() — запись пустого набора делает это автоматически.
  3. Исключаются лишние блокировки при чтении, что спасает от классических дедлоков.
  4. Метод работает универсально и генерирует наиболее оптимальный SQL-запрос на уровне СУБД.

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

← На главную